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