Skip to content

Membership functions

A membership function (MF) is any callable mapping a crisp value (scalar or NumPy array) to a degree in [0, 1]. The built-in shapes:

Factory Shape Parameters
fz.tri(a, b, c) triangular feet a/c, peak b
fz.trap(a, b, c, d) trapezoidal shoulders a/d, flat top b..c
fz.gauss(c, sigma) gaussian center c, spread sigma
fz.gauss2(c1, s1, c2, s2) two-sided gaussian plateau c1..c2, own spread each side
fz.gbell(a, b, c) generalized bell width a, slope b, center c
fz.sigmoid(a, c) sigmoidal slope a, inflection c
fz.smf(a, b) S-shaped spline smooth rise a → b
fz.zmf(a, b) Z-shaped spline smooth fall a → b
fz.pimf(a, b, c, d) Π-shaped S-rise a..b, plateau, Z-fall c..d
fz.singleton(v) crisp singleton 1 at v, 0 elsewhere
fz.ramp_up(a, b) / fz.ramp_down(a, b) linear ramps monotonic

The shapes match the ones in MATLAB's Fuzzy Logic Toolbox, so a model written there ports over without redrawing its partitions.

import numpy as np, fuzzytool as fz

m = fz.gauss(5, 1.5)
m(5)                       # 1.0
m(np.array([3, 5, 7]))     # vectorized

Numerical stability

fz.sigmoid uses an overflow-safe logistic: the exponential is only ever evaluated on non-positive arguments, so even very large inputs saturate cleanly to 0 or 1 without a RuntimeWarning. Output is unchanged within the numerically safe range.

Custom shapes

Any callable works — no registration needed:

def ramp(x):
    return np.clip(np.asarray(x, float) / 10, 0, 1)

var = fz.Variable("x", (0, 10))
var["high"] = ramp

This is the core extensibility idea: the inference engine only relies on the MembershipFunction Protocol (x -> degree), so a new shape is just a new callable.

To let a custom shape survive fz.save / fz.load, register the class with the constructor arguments that rebuild it:

class Cosine:
    def __init__(self, c, w):
        self.c, self.w = float(c), float(w)
    def __call__(self, x):
        d = np.abs(np.asarray(x, float) - self.c) / self.w
        return np.where(d <= 1, 0.5 * (1 + np.cos(np.pi * d)), 0.0)

fz.membership.register("cosine", Cosine, ("c", "w"))

Nothing else changes: systems using Cosine now round-trip through JSON.

Monotonic shapes and Tsukamoto

sigmoid, smf, zmf, ramp_up and ramp_down are monotonic, so they also expose inverse(degree) — the crisp value at which membership equals a given degree. That is exactly what a Tsukamoto consequent needs:

fz.Tsukamoto().rule(x["small"], fz.smf(0, 10))

Complete partitions with shoulders

Auto-generated terms drop to zero at the edges of the universe, which leaves the extremes driven by a single weak rule. shoulders=True saturates the outermost terms instead:

fz.Variable("v", (0, 10), terms=["lo", "mid", "hi"], shoulders=True)

Now lo is 1 everywhere left of its center and hi is 1 everywhere right of its own, so every point of the universe is covered at full strength by something. audit reports the holes you get without it.