diff --git a/docs/src/changelog.md b/docs/src/changelog.md index 38a1b7a..61780bb 100644 --- a/docs/src/changelog.md +++ b/docs/src/changelog.md @@ -4,6 +4,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## Unreleased +- ![](https://img.shields.io/badge/BREAKING-red.svg) use a different type parameter for each field of membership functions. This allows membership functions to support mixed-type inputs like `GaussianMF(0, 0.1)`. + +## v0.1.3 -- 2024-09-18 + +[view release on GitHub](https://github.com/lucaferranti/FuzzyLogic.jl/releases/tag/v0.1.3) + - ![](https://img.shields.io/badge/bugfix-purple.svg) fix bug in Julia code generation of ZShape and SShape mf - ![](https://img.shields.io/badge/bugfix-purple.svg) disallow implicit conversion from interval to float - ![](https://img.shields.io/badge/new%20feature-green.svg) added semi-elliptic and singleton membership functions diff --git a/src/membership_functions.jl b/src/membership_functions.jl index 529318d..0550168 100644 --- a/src/membership_functions.jl +++ b/src/membership_functions.jl @@ -2,6 +2,16 @@ abstract type AbstractPredicate end abstract type AbstractMembershipFunction <: AbstractPredicate end +function Base.show(io::IO, mf::MF) where {MF <: AbstractMembershipFunction} + props = [getproperty(mf, x) for x in fieldnames(MF)] + print(io, MF.name.name, "(") + for (i, p) in enumerate(props) + print(io, p) + i < length(props) && print(io, ", ") + end + print(io, ")") +end + """ Singleton membership function. Equal to one at a single point and zero elsewhere. @@ -35,13 +45,13 @@ $(TYPEDFIELDS) mf = GeneralizedBellMF(2, 4, 5) ``` """ -struct GeneralizedBellMF{T <: Real, S <: Real} <: AbstractMembershipFunction +struct GeneralizedBellMF{Ta <: Real, Tb <: Real, Tc <: Real} <: AbstractMembershipFunction "Width of the curve, the bigger the wider." - a::T + a::Ta "Slope of the curve, the bigger the steeper." - b::S + b::Tb "Center of the curve." - c::T + c::Tc end (mf::GeneralizedBellMF)(x) = 1 / (1 + abs((x - mf.c) / mf.a)^(2mf.b)) @@ -58,11 +68,11 @@ $(TYPEDFIELDS) mf = GaussianMF(5.0, 1.5) ``` """ -struct GaussianMF{T <: Real} <: AbstractMembershipFunction +struct GaussianMF{Tm <: Real, Ts <: Real} <: AbstractMembershipFunction "mean ``μ``." - mu::T + mu::Tm "standard deviation ``σ``." - sig::T + sig::Ts end (mf::GaussianMF)(x) = exp(-(x - mf.mu)^2 / (2mf.sig^2)) @@ -79,13 +89,13 @@ $(TYPEDFIELDS) mf = TriangularMF(3, 5, 7) ``` """ -struct TriangularMF{T <: Real} <: AbstractMembershipFunction +struct TriangularMF{Ta <: Real, Tb <: Real, Tc <: Real} <: AbstractMembershipFunction "left foot." - a::T + a::Ta "peak." - b::T + b::Tb "right foot." - c::T + c::Tc end (mf::TriangularMF)(x) = max(min((x - mf.a) / (mf.b - mf.a), (mf.c - x) / (mf.c - mf.b)), 0) @@ -102,15 +112,16 @@ $(TYPEDFIELDS) mf = TrapezoidalMF(1, 3, 7, 9) ``` """ -struct TrapezoidalMF{T <: Real} <: AbstractMembershipFunction +struct TrapezoidalMF{Ta <: Real, Tb <: Real, Tc <: Real, Td <: Real} <: + AbstractMembershipFunction "left foot." - a::T + a::Ta "left shoulder." - b::T + b::Tb "right shoulder." - c::T + c::Tc "right foot." - d::T + d::Td end function (mf::TrapezoidalMF)(x) return max(min((x - mf.a) / (mf.b - mf.a), 1, (mf.d - x) / (mf.d - mf.c)), 0) @@ -130,11 +141,11 @@ $(TYPEDFIELDS) mf = LinearMF(2, 8) ``` """ -struct LinearMF{T <: Real} <: AbstractMembershipFunction +struct LinearMF{Ta <: Real, Tb <: Real} <: AbstractMembershipFunction "foot." - a::T + a::Ta "shoulder." - b::T + b::Tb end (mf::LinearMF)(x) = max(min((x - mf.a) / (mf.b - mf.a), 1), 0) @@ -151,11 +162,11 @@ $(TYPEDFIELDS) mf = SigmoidMF(2, 5) ``` """ -struct SigmoidMF{T <: Real} <: AbstractMembershipFunction +struct SigmoidMF{Ta <: Real, Tc <: Real} <: AbstractMembershipFunction "parameter controlling the slope of the curve." - a::T + a::Ta "center of the slope." - c::T + c::Tc end (mf::SigmoidMF)(x) = 1 / (1 + exp(-mf.a * (x - mf.c))) @@ -172,19 +183,22 @@ $(TYPEDFIELDS) mf = DifferenceSigmoidMF(5, 2, 5, 7) ``` """ -struct DifferenceSigmoidMF{T <: Real} <: AbstractMembershipFunction +struct DifferenceSigmoidMF{Ta1 <: Real, Tc1 <: Real, Ta2 <: Real, Tc2 <: Real} <: + AbstractMembershipFunction "slope of the first sigmoid." - a1::T + a1::Ta1 "center of the first sigmoid." - c1::T + c1::Tc1 "slope of the second sigmoid." - a2::T + a2::Ta2 "center of the second sigmoid." - c2::T + c2::Tc2 end function (mf::DifferenceSigmoidMF)(x) - return max(min(1 / (1 + exp(-mf.a1 * (x - mf.c1))) - - 1 / (1 + exp(-mf.a2 * (x - mf.c2))), 1), 0) + return max( + min(1 / (1 + exp(-mf.a1 * (x - mf.c1))) - + 1 / (1 + exp(-mf.a2 * (x - mf.c2))), 1), + 0) end """ @@ -227,11 +241,11 @@ $(TYPEDFIELDS) mf = SShapeMF(1, 8) ``` """ -struct SShapeMF{T <: Real} <: AbstractMembershipFunction +struct SShapeMF{Ta <: Real, Tb <: Real} <: AbstractMembershipFunction "foot." - a::T + a::Ta "shoulder." - b::T + b::Tb end function (s::SShapeMF)(x::T) where {T <: Real} x <= s.a && return zero(float(T)) @@ -253,11 +267,11 @@ $(TYPEDFIELDS) mf = ZShapeMF(3, 7) ``` """ -struct ZShapeMF{T <: Real} <: AbstractMembershipFunction +struct ZShapeMF{Ta <: Real, Tb <: Real} <: AbstractMembershipFunction "shoulder." - a::T + a::Ta "foot." - b::T + b::Tb end function (z::ZShapeMF)(x::T) where {T <: Real} x <= z.a && return one(float(T)) @@ -279,15 +293,16 @@ $(TYPEDFIELDS) mf = PiShapeMF(1, 4, 5, 10) ``` """ -struct PiShapeMF{T <: Real} <: AbstractMembershipFunction +struct PiShapeMF{Ta <: Real, Tb <: Real, Tc <: Real, Td <: Real} <: + AbstractMembershipFunction "left foot." - a::T + a::Ta "left shoulder." - b::T + b::Tb "right shoulder." - c::T + c::Tc "right foot." - d::T + d::Td end function (p::PiShapeMF)(x::T) where {T <: Real} (x <= p.a || x >= p.d) && return zero(float(T)) @@ -311,11 +326,11 @@ $(TYPEDFIELDS) mf = SemiEllipticMF(5.0, 4.0) ``` """ -struct SemiEllipticMF{T <: Real} <: AbstractMembershipFunction +struct SemiEllipticMF{Tcd <: Real, Trd <: Real} <: AbstractMembershipFunction "center." - cd::T + cd::Tcd "semi-axis." - rd::T + rd::Trd end function (semf::SemiEllipticMF)(x::Real) cd, rd = semf.cd, semf.rd diff --git a/src/parser.jl b/src/parser.jl index 7c21f83..b10f637 100644 --- a/src/parser.jl +++ b/src/parser.jl @@ -49,21 +49,21 @@ tipper Inputs: ------- service ∈ [0, 10] with membership functions: - poor = GaussianMF{Float64}(0.0, 1.5) - good = GaussianMF{Float64}(5.0, 1.5) - excellent = GaussianMF{Float64}(10.0, 1.5) + poor = GaussianMF(0.0, 1.5) + good = GaussianMF(5.0, 1.5) + excellent = GaussianMF(10.0, 1.5) food ∈ [0, 10] with membership functions: - rancid = TrapezoidalMF{Int64}(-2, 0, 1, 3) - delicious = TrapezoidalMF{Int64}(7, 9, 10, 12) + rancid = TrapezoidalMF(-2, 0, 1, 3) + delicious = TrapezoidalMF(7, 9, 10, 12) Outputs: -------- tip ∈ [0, 30] with membership functions: - cheap = TriangularMF{Int64}(0, 5, 10) - average = TriangularMF{Int64}(10, 15, 20) - generous = TriangularMF{Int64}(20, 25, 30) + cheap = TriangularMF(0, 5, 10) + average = TriangularMF(10, 15, 20) + generous = TriangularMF(20, 25, 30) Inference rules: @@ -128,13 +128,13 @@ tipper Inputs: ------- service ∈ [0, 10] with membership functions: - poor = GaussianMF{Float64}(0.0, 1.5) - good = GaussianMF{Float64}(5.0, 1.5) - excellent = GaussianMF{Float64}(10.0, 1.5) + poor = GaussianMF(0.0, 1.5) + good = GaussianMF(5.0, 1.5) + excellent = GaussianMF(10.0, 1.5) food ∈ [0, 10] with membership functions: - rancid = TrapezoidalMF{Int64}(-2, 0, 1, 3) - delicious = TrapezoidalMF{Int64}(7, 9, 10, 12) + rancid = TrapezoidalMF(-2, 0, 1, 3) + delicious = TrapezoidalMF(7, 9, 10, 12) Outputs: @@ -170,7 +170,7 @@ function _fis(ex::Expr, type) inputs, outputs, opts, rules = parse_body(body, argsin, argsout, type) fis = :($type(; name = $(QuoteNode(name)), inputs = $inputs, - outputs = $outputs, rules = $rules)) + outputs = $outputs, rules = $rules)) append!(fis.args[2].args, opts) return fis end @@ -227,21 +227,23 @@ function parse_body(body, argsin, argsout, type) end function parse_line!(inputs, outputs, rules, opts, line, argsin, argsout, type) - if @capture(line, var_:=begin args__ end) + if @capture(line, var_:=begin + args__ + end) var = to_var_name(var) if var in argsin push!(inputs.args[2].args, parse_variable(var, args)) elseif var in argsout # TODO: makes this more scalable push!(outputs.args[2].args, - type == :SugenoFuzzySystem ? parse_sugeno_output(var, args, argsin) : - parse_variable(var, args)) + type == :SugenoFuzzySystem ? parse_sugeno_output(var, args, argsin) : + parse_variable(var, args)) else throw(ArgumentError("Undefined variable $var")) end elseif @capture(line, for i_ in start_:stop_ - sts__ - end) + sts__ + end) for j in start:stop for st in sts ex = MacroTools.postwalk(x -> x == i ? j : x, st) @@ -282,10 +284,10 @@ function parse_antecedent(ant) return Expr(:call, :FuzzyOr, parse_antecedent(left), parse_antecedent(right)) elseif @capture(ant, subj_==prop_) return Expr(:call, :FuzzyRelation, QuoteNode(to_var_name(subj)), - QuoteNode(to_var_name(prop))) + QuoteNode(to_var_name(prop))) elseif @capture(ant, subj_!=prop_) return Expr(:call, :FuzzyNegation, QuoteNode(to_var_name(subj)), - QuoteNode(to_var_name(prop))) + QuoteNode(to_var_name(prop))) else throw(ArgumentError("Invalid premise $ant")) end @@ -295,7 +297,7 @@ function parse_consequents(cons) newcons = map(cons) do c @capture(c, subj_==prop_) || throw(ArgumentError("Invalid consequence $c")) Expr(:call, :FuzzyRelation, QuoteNode(to_var_name(subj)), - QuoteNode(to_var_name(prop))) + QuoteNode(to_var_name(prop))) end return Expr(:vect, newcons...) end diff --git a/test/test_membership_functions.jl b/test/test_membership_functions.jl index 3632db3..36b4957 100644 --- a/test/test_membership_functions.jl +++ b/test/test_membership_functions.jl @@ -166,3 +166,11 @@ end @test mf(4) == Interval(0.0, 0.0) @test mf(5) == Interval(0.0, 0.0) end + +@testset "Mixed argument types" begin + mf = TriangularMF(1, 2.0, 3.0f0) + @test typeof(mf) == TriangularMF{Int64, Float64, Float32} + + mf = TrapezoidalMF(1, 2.0, 3, 4.0) + @test typeof(mf) == TrapezoidalMF{Int64, Float64, Int64, Float64} +end