Skip to content

API reference

Generated from docstrings with mkdocstrings.

Membership functions

fuzzytool.membership

Membership functions.

Every membership function (MF) is a callable mapping a crisp value (scalar or NumPy array) to a membership degree in [0, 1]. The :class:MembershipFunction Protocol is the only contract the rest of the library relies on: the inference engine never inspects a concrete MF type, so a new shape = a new callable, with no changes to the core (mirroring the "one variant = one impl" design of the sibling project turboswarm).

The lowercase factory functions (:func:tri, :func:trap, :func:gauss, :func:gauss2, :func:gbell, :func:sigmoid, :func:smf, :func:zmf, :func:pimf, :func:singleton, :func:ramp_up, :func:ramp_down) are the public API; they return small classes so the parameters stay introspectable for visualization and serialization.

Shapes that are monotonic (:func:sigmoid, :func:smf, :func:zmf, :func:ramp_up, :func:ramp_down) also expose inverse(degree), which makes them usable as Tsukamoto consequents. Custom shapes join the serialization round trip through :func:register.

MembershipFunction

Bases: Protocol

A callable x -> degree with membership degrees in [0, 1].

Triangular

Triangular MF with feet at a/c and peak at b.

Trapezoidal

Trapezoidal MF; flat top between b and c.

Gaussian

Gaussian MF centered at c with spread sigma.

GeneralizedBell

Generalized bell MF: 1 / (1 + |(x - c) / a|^(2b)).

Sigmoid

Sigmoidal MF: 1 / (1 + exp(-a (x - c))). Monotonic (invertible).

inverse

inverse(y: float) -> float

The x at which membership equals y (for Tsukamoto inference).

SShaped

S-shaped (spline) MF: smoothly 0 at a rising to 1 at b. Monotonic.

inverse

inverse(y: float) -> float

The x at which membership equals y (for Tsukamoto inference).

ZShaped

Z-shaped (spline) MF: smoothly 1 at a falling to 0 at b. Monotonic.

inverse

inverse(y: float) -> float

The x at which membership equals y (for Tsukamoto inference).

PiShaped

Pi-shaped MF: an S-shaped rise a..b and a Z-shaped fall c..d.

Gaussian2

Two-sided Gaussian: left shoulder (c1, sigma1), right (c2, sigma2).

Membership is 1 on the plateau [c1, c2]; outside it, each side decays with its own Gaussian. With c1 == c2 it reduces to a plain Gaussian with asymmetric spreads.

Singleton

Crisp singleton: 1 at value (within tol), 0 elsewhere.

Useful as a Mamdani consequent (which then behaves like a zero-order TSK rule) and for fuzzifying already-crisp measurements.

RampUp

Monotonically increasing ramp: 0 below a, 1 above b.

RampDown

Monotonically decreasing ramp: 1 below a, 0 above b.

tri

tri(a: float, b: float, c: float) -> Triangular

Triangular MF: feet at a/c, peak at b.

trap

trap(a: float, b: float, c: float, d: float) -> Trapezoidal

Trapezoidal MF: shoulders a/d, flat top b..c.

gauss

gauss(c: float, sigma: float) -> Gaussian

Gaussian MF: center c, spread sigma.

gbell

gbell(a: float, b: float, c: float) -> GeneralizedBell

Generalized bell MF: width a, slope b, center c.

sigmoid

sigmoid(a: float, c: float) -> Sigmoid

Sigmoidal MF: slope a, inflection c.

smf

smf(a: float, b: float) -> SShaped

S-shaped MF: smooth monotonic rise from 0 at a to 1 at b.

zmf

zmf(a: float, b: float) -> ZShaped

Z-shaped MF: smooth monotonic fall from 1 at a to 0 at b.

pimf

pimf(a: float, b: float, c: float, d: float) -> PiShaped

Pi-shaped MF: S-shaped rise a..b, plateau, Z-shaped fall c..d.

gauss2

gauss2(c1: float, sigma1: float, c2: float, sigma2: float) -> Gaussian2

Two-sided Gaussian MF with a plateau between c1 and c2.

singleton

singleton(value: float, tol: float = 1e-09) -> Singleton

Crisp singleton MF: 1 at value, 0 elsewhere.

ramp_up

ramp_up(a: float, b: float) -> RampUp

Increasing ramp MF from 0 at a to 1 at b (monotonic).

ramp_down

ramp_down(a: float, b: float) -> RampDown

Decreasing ramp MF from 1 at a to 0 at b (monotonic).

register

register(tag: str, cls: type, params: tuple[str, ...]) -> None

Make a custom membership-function class serializable under tag.

params names the constructor arguments, in order, as attributes of the instance — so cls(*[getattr(obj, p) for p in params]) rebuilds it. This is the extension point that lets :func:fuzzytool.save / :func:fuzzytool.load round-trip shapes that are not built in.

import numpy as np, fuzzytool as fz 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, dtype=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")) fz.membership.to_dict(Cosine(2, 1))

register_factory

register_factory(tag: str, factory) -> None

Register a factory that rebuilds a shape from its serialized parameters.

Used for membership functions produced by a function rather than a class — the objects mark themselves with a _serial_spec attribute naming this tag, and :func:from_dict calls the factory to rebuild them.

to_dict

to_dict(mf) -> dict

Serialize a registered membership function to a JSON-ready dict.

from_dict

from_dict(d: dict)

Rebuild a membership function from :func:to_dict output.

Connectives (t-norms / s-norms)

fuzzytool.norms

Fuzzy connectives: t-norms (AND), s-norms / t-conorms (OR), complements (NOT).

Connectives are plain vectorized callables (a, b) -> result operating elementwise on membership degrees. They are pluggable: the inference engines look them up by name through :func:get_tnorm / :func:get_snorm / :func:get_complement, so adding a connective means registering one function — the engine never changes.

Besides the named connectives there are parametric families — Yager, Dombi and Frank — exposed as factories that return a callable, which every engine accepts in place of a name::

>>> import fuzzytool as fz
>>> sys = fz.Mamdani(tnorm=fz.norms.yager_tnorm(2.0))

Norm

Bases: Protocol

A binary connective on membership degrees.

t_min

t_min(a, b)

Minimum (Gödel) t-norm.

t_prod

t_prod(a, b)

Product (algebraic) t-norm.

t_lukasiewicz

t_lukasiewicz(a, b)

Łukasiewicz t-norm: max(0, a + b - 1).

t_einstein

t_einstein(a, b)

Einstein product: a b / (2 - (a + b - a b)).

t_hamacher

t_hamacher(a, b)

Hamacher product: a b / (a + b - a b) (0 when both are 0).

t_drastic

t_drastic(a, b)

Drastic product: b if a == 1, a if b == 1, else 0.

t_nilpotent

t_nilpotent(a, b)

Nilpotent minimum: min(a, b) if a + b > 1, else 0.

s_max

s_max(a, b)

Maximum (Gödel) s-norm.

s_probor

s_probor(a, b)

Probabilistic OR (algebraic sum): a + b - a*b.

s_lukasiewicz

s_lukasiewicz(a, b)

Łukasiewicz s-norm: min(1, a + b).

s_einstein

s_einstein(a, b)

Einstein sum: (a + b) / (1 + a b).

s_hamacher

s_hamacher(a, b)

Hamacher sum: (a + b - 2 a b) / (1 - a b) (1 when both are 1).

s_drastic

s_drastic(a, b)

Drastic sum: b if a == 0, a if b == 0, else 1.

s_nilpotent

s_nilpotent(a, b)

Nilpotent maximum: max(a, b) if a + b < 1, else 1.

yager_tnorm

yager_tnorm(p: float) -> Callable

Yager t-norm: max(0, 1 - ((1-a)^p + (1-b)^p)^(1/p)) (p > 0).

yager_snorm

yager_snorm(p: float) -> Callable

Yager s-norm: min(1, (a^p + b^p)^(1/p)) (p > 0).

dombi_tnorm

dombi_tnorm(p: float) -> Callable

Dombi t-norm with parameter p > 0 (0 whenever either degree is 0).

dombi_snorm

dombi_snorm(p: float) -> Callable

Dombi s-norm with parameter p > 0 (1 whenever either degree is 1).

frank_tnorm

frank_tnorm(s: float) -> Callable

Frank t-norm with parameter s > 0, s != 1.

frank_snorm

frank_snorm(s: float) -> Callable

Frank s-norm, the De Morgan dual of :func:frank_tnorm.

get_tnorm

get_tnorm(name: str | Callable) -> Callable

Resolve a t-norm by name (or pass a callable through unchanged).

get_snorm

get_snorm(name: str | Callable) -> Callable

Resolve an s-norm by name (or pass a callable through unchanged).

complement

complement(a)

Standard fuzzy complement (NOT): 1 - a.

sugeno_complement

sugeno_complement(lam: float) -> Callable

Sugeno complement (1 - a) / (1 + lam a) (lam > -1).

lam = 0 recovers the standard complement.

yager_complement

yager_complement(w: float) -> Callable

Yager complement (1 - a^w)^(1/w) (w > 0).

w = 1 recovers the standard complement.

get_complement

get_complement(name: str | Callable) -> Callable

Resolve a complement by name (or pass a callable through unchanged).

import fuzzytool as fz float(fz.norms.get_complement("standard")(0.25)) 0.75

Sets, variables, and rule antecedents

fuzzytool.sets

Fuzzy sets, linguistic variables, and the rule-antecedent expression tree.

A :class:Variable is a linguistic variable: a named universe of discourse plus a dictionary of terms (named membership functions). Indexing a variable with a term name (score["good"]) returns a :class:Proposition, the atom of a rule antecedent. Propositions compose with Python operators:

score["poor"] | dti["high"]         # OR  (s-norm)
score["good"] & dti["low"]          # AND (t-norm)
~dti["high"]                        # NOT (complement)

The result is a small expression tree that the inference engine evaluates against crisp inputs to get a firing strength. The same atom doubles as a rule consequent (premium["high"]), so there is a single concept to learn.

Antecedent

Base node of a rule-antecedent expression tree.

eval

eval(inputs: Mapping[str, float], tnorm: Callable, snorm: Callable, *, complement: Callable | None = None, cache: dict | None = None) -> np.ndarray

Return the (type-1) firing strength given crisp inputs.

Parameters:

Name Type Description Default
inputs Mapping[str, float]

crisp values per variable name (scalars or arrays).

required
tnorm Callable

connective used by :class:And nodes.

required
snorm Callable

connective used by :class:Or nodes.

required
complement Callable | None

connective used by :class:Not nodes; defaults to the standard 1 - a.

None
cache dict | None

optional dict memoizing (variable, term) degrees across the whole rule base. A rule base repeats the same propositions many times, so the engines pass one dict per call and evaluate every membership function at most once.

None

eval_interval

eval_interval(inputs: Mapping[str, float], tnorm: Callable, snorm: Callable, *, complement: Callable | None = None, cache: dict | None = None)

Return the firing interval (lower, upper) for IT2 inference.

Type-1 terms collapse to a degenerate interval (d, d), so type-1 and IT2 terms may be mixed freely in the same antecedent. Takes the same complement / cache options as :meth:eval.

propositions

propositions() -> list[Proposition]

Every :class:Proposition atom in this subtree, in reading order.

to_dict

to_dict() -> dict

Serialize this antecedent subtree to a JSON-ready dict.

Proposition

Bases: Antecedent

An atomic variable is term clause; also used as a consequent.

Variable

A linguistic variable: a named universe with named fuzzy-set terms.

Parameters:

Name Type Description Default
name str

identifier; rule inputs are matched to variables by this name.

required
universe tuple[float, float]

(low, high) range of the universe of discourse.

required
terms Iterable[str] | Mapping[str, MembershipFunction] | None

optional list of term names to auto-generate evenly across the universe, or a mapping {name: MembershipFunction}.

None
kind str

shape used by auto-generation ("triangular" or "gauss").

'triangular'
resolution int

number of samples used to discretize the universe for Mamdani defuzzification.

501
shoulders bool

saturate the outermost auto-generated terms at the edges of the universe (see :meth:auto_terms).

False

auto_terms

auto_terms(names: list[str], kind: str = 'triangular', shoulders: bool = False) -> Variable

Generate evenly-spaced terms named names across the universe.

Parameters:

Name Type Description Default
names list[str]

term names, laid out left to right.

required
kind str

"triangular" or "gauss".

'triangular'
shoulders bool

make the first and last term shouldered — saturated at 1 all the way to the edge of the universe instead of decaying past their center. This is the usual practice for a complete partition: without it, membership drops toward the extremes and inference near the edges is driven by a single weak rule.

False

v = Variable("v", (0, 10), terms=["lo", "hi"], shoulders=True) float(v.fuzzify(0.0)["lo"]), float(v.fuzzify(10.0)["hi"]) (1.0, 1.0)

fuzzify

fuzzify(x) -> dict[str, np.ndarray]

Membership degree of x in every term, as {term: degree}.

IT2 terms collapse to the midpoint of their membership interval, so the result is always a plain number (or array) per term.

v = Variable("v", (0, 10), terms=["lo", "hi"]) {k: round(float(d), 2) for k, d in v.fuzzify(2.5).items()}

to_dict

to_dict() -> dict

Serialize this variable (universe + terms) to a JSON-ready dict.

Only built-in membership functions can be serialized (see :func:fuzzytool.membership.to_dict).

from_dict classmethod

from_dict(d: dict) -> Variable

Rebuild a variable from :meth:to_dict output.

antecedent_from_dict

antecedent_from_dict(d: dict, variables: Mapping[str, Variable]) -> Antecedent

Rebuild an antecedent tree from :meth:Antecedent.to_dict output.

variables maps variable names to the :class:Variable instances the propositions should reference.

Rules

fuzzytool.rules

Fuzzy rules shared by the inference engines.

A :class:Rule pairs an antecedent expression with a consequent and an optional weight. The consequent is interpreted by the engine: Mamdani expects a :class:~fuzzytool.sets.Proposition (output is term); TSK expects a constant or a linear coefficient vector.

Rule dataclass

IF antecedent THEN consequent with an optional firing weight.

Defuzzification

fuzzytool.defuzz

Defuzzification: collapse an aggregated output set into a crisp value.

Each defuzzifier takes the discretized universe x and the aggregated membership y (same shape) and returns a scalar. They are looked up by name through :func:get_defuzzifier, so adding a method = registering a function.

centroid

centroid(x: ndarray, y: ndarray) -> float

Center of gravity of the area under y (the most common choice).

bisector

bisector(x: ndarray, y: ndarray) -> float

Abscissa that splits the area under y into two equal halves.

mom

mom(x: ndarray, y: ndarray) -> float

Mean of maxima.

som

som(x: ndarray, y: ndarray) -> float

Smallest of maxima.

lom

lom(x: ndarray, y: ndarray) -> float

Largest of maxima.

weighted_average

weighted_average(x: ndarray, y: ndarray) -> float

Height / weighted-average defuzzification: Σ x·y / Σ y.

Unlike :func:centroid this treats the samples as discrete weighted points rather than integrating the area, so it is cheaper and insensitive to the universe's resolution being non-uniform.

coa_largest

coa_largest(x: ndarray, y: ndarray) -> float

Center of the largest contiguous area (COA / CLA).

When aggregation leaves several disjoint humps, the centroid can land in the valley between them — a value no rule ever supported. This method keeps only the widest connected region of positive membership and takes its centroid.

centroid_batch

centroid_batch(x: ndarray, Y: ndarray) -> np.ndarray

Vectorized :func:centroid over the rows of Y.

bisector_batch

bisector_batch(x: ndarray, Y: ndarray) -> np.ndarray

Vectorized :func:bisector over the rows of Y.

weighted_average_batch

weighted_average_batch(x: ndarray, Y: ndarray) -> np.ndarray

Vectorized :func:weighted_average over the rows of Y.

get_batch_defuzzifier

get_batch_defuzzifier(fn: Callable) -> Callable | None

Return the vectorized form of a defuzzifier, or None if it has none.

get_defuzzifier

get_defuzzifier(name: str | Callable) -> Callable

Resolve a defuzzifier by name (or pass a callable through unchanged).

Inference — Mamdani

fuzzytool.inference.mamdani

Mamdani fuzzy inference.

The engine knows nothing about specific membership functions, connectives, or defuzzifiers: t-norm, s-norm, implication, aggregation and defuzzification are all resolved by name (or supplied as callables) and applied uniformly. Adding a behavior means registering a function in :mod:fuzzytool.norms or :mod:fuzzytool.defuzz, never editing this loop.

Pipeline per call: evaluate each rule's antecedent to a firing strength → shape its consequent term over the output universe via the implication operator → aggregate the shaped sets per output variable → defuzzify.

Mamdani

Bases: RuleBase

A Mamdani inference system.

Parameters:

Name Type Description Default
tnorm str | Callable

t-norm for AND in antecedents (default "min").

'min'
snorm str | Callable

s-norm for OR in antecedents (default "max").

'max'
implication str

how a firing strength shapes its consequent set — "min" (clip) or "prod" (scale).

'min'
aggregation str | Callable

s-norm combining shaped sets per output (default "max").

'max'
defuzz str | Callable

defuzzification method (default "centroid").

'centroid'
on_no_rule str

crisp value for an output when no rule fires — "mid" (midpoint of the output universe, the default), "nan", or "raise". Applied identically by __call__ and predict.

'mid'
complement str | Callable

connective for NOT in antecedents (default "standard", i.e. 1 - a); accepts a name or a callable.

'standard'

rule

rule(antecedent: Antecedent, consequent: Proposition, weight: float = 1.0) -> Mamdani

Add IF antecedent THEN output is term and return self.

__call__

__call__(**inputs: float)

Run inference. Returns a float for one output, else a dict by name.

predict

predict(*, chunk_size: int | None = 4096, **inputs: ndarray)

Vectorized inference over array-valued inputs.

Each keyword is an array of n samples. Returns an array of length n for a single output, else a dict of arrays. Equivalent to calling the system once per sample, but evaluated in batch.

Parameters:

Name Type Description Default
chunk_size int | None

number of samples processed per block. Mamdani inference materializes an (n_samples, resolution) array per output, so a large n would otherwise exhaust memory; chunking bounds the peak at chunk_size * resolution floats. None disables it.

4096
**inputs ndarray

one array per input variable.

{}

Inference — Takagi-Sugeno (TSK)

fuzzytool.inference.tsk

Takagi-Sugeno-Kang (TSK) fuzzy inference.

Consequents are crisp functions of the inputs rather than fuzzy sets, so there is no defuzzification: the output is the firing-weighted average of the rule consequents. A consequent may be

  • a number — zero-order (Sugeno constant), e.g. 5.0;
  • a mapping {"const": b0, "x": b1, ...} — first-order linear in the inputs;
  • any callable f(**inputs) -> float — arbitrary.

TSK

Bases: RuleBase

A (zero- or first-order) Takagi-Sugeno inference system.

Parameters:

Name Type Description Default
tnorm str | Callable

t-norm for AND in antecedents (default "min").

'min'
snorm str | Callable

s-norm for OR in antecedents (default "max").

'max'
on_no_rule str

what to return when no rule fires — "nan" (default) or "raise". Applied identically by __call__ and predict.

'nan'
complement str | Callable

connective for NOT in antecedents (default "standard").

'standard'

rule

rule(antecedent: Antecedent, consequent, weight: float = 1.0) -> TSK

Add IF antecedent THEN output = consequent and return self.

__call__

__call__(**inputs: float) -> float

Run inference, returning the firing-weighted average output.

predict

predict(**inputs) -> np.ndarray

Vectorized inference over array-valued inputs (n per keyword).

Returns an array of length n. Samples where no rule fires follow on_no_rule: nan (default) or a raised ValueError.

Inference — Tsukamoto

fuzzytool.inference.tsukamoto

Tsukamoto fuzzy inference.

In a Tsukamoto system every rule's consequent is a monotonic membership function. A rule firing with strength w produces the crisp value at which its consequent reaches w (the inverse of the monotonic MF). The system output is the firing-weighted average of those crisp values — so, like TSK, there is no defuzzification step.

Consequents must expose inverse(degree) -> value; the built-in :func:~fuzzytool.membership.ramp_up, :func:~fuzzytool.membership.ramp_down and :func:~fuzzytool.membership.sigmoid do.

Tsukamoto

Bases: RuleBase

A Tsukamoto inference system (monotonic consequents).

Parameters:

Name Type Description Default
tnorm str | Callable

t-norm for AND in antecedents (default "min").

'min'
snorm str | Callable

s-norm for OR in antecedents (default "max").

'max'
on_no_rule str

what to return when no rule fires — "nan" (default) or "raise".

'nan'
complement str | Callable

connective for NOT in antecedents (default "standard").

'standard'

rule

rule(antecedent: Antecedent, consequent, weight: float = 1.0) -> Tsukamoto

Add a rule; consequent is a monotonic MF with an inverse.

__call__

__call__(**inputs: float) -> float

Run inference, returning the firing-weighted average crisp output.

predict

predict(**inputs) -> np.ndarray

Vectorized inference over array-valued inputs (n per keyword).

Returns an array of length n. The consequent inverses are scalar functions, so they are applied per sample; the antecedents are still evaluated in batch.

Inference — shared rule-base behavior

fuzzytool.inference._common

Shared helpers and the common rule-base behavior of the inference engines.

RuleBase

Behavior every engine shares: introspection, firing strengths, explanation.

Engines mixing this in must expose rules, tnorm, snorm and complement. Nothing here knows how a consequent is interpreted, so the same code serves Mamdani, TSK, Tsukamoto and the type-2 engines.

input_variables

input_variables() -> dict

Input linguistic variables referenced by the antecedents, by name.

output_variables

output_variables() -> dict

Output linguistic variables, by name (empty for engines without them).

rule_from_text

rule_from_text(text: str, variables: object = None) -> object

Add a rule written in the text syntax of :mod:fuzzytool.dsl.

Parameters:

Name Type Description Default
text str

e.g. "IF score IS poor OR dti IS high THEN premium IS high".

required
variables object

the variables the rule may reference. Defaults to the ones this system already knows, so only the first rule of a fresh system needs to pass them.

None

Returns self, so calls chain like :meth:rule.

firing

firing(**inputs) -> np.ndarray

Weighted firing strength of every rule, in rule order.

Evaluates the whole rule base with one shared memo, so each membership function is applied at most once even when many rules mention it. Accepts scalars (returns shape (n_rules,)) or arrays (returns shape (n_rules, n_samples)).

explain

explain(**inputs) -> dict

Run the system on inputs and report why it answered that.

Returns the crisp output plus fired_rules: every rule with a positive firing strength, strongest first, each carrying its index, its text, its firing strength and its share of the total firing. This is the payload a human (or an LLM agent) needs to trust the answer.

from fuzzytool import datasets sys, *_ = datasets.credit_risk() report = sys.explain(score=520, dti=42) max(r["share"] for r in report["fired_rules"]) > 0.5 True

summary

summary() -> str

A readable listing of the rule base (variables, terms, rules).

check_on_no_rule

check_on_no_rule(value: str, *, allow_mid: bool) -> str

Validate an on_no_rule policy, returning it unchanged.

Parameters:

Name Type Description Default
value str

the policy string to check.

required
allow_mid bool

whether "mid" is a valid option (true only for engines with an output universe, i.e. Mamdani).

required

The uniform policy across engines answers "what should a call return when no rule fires for an output?": "nan" (a quiet, composable sentinel), "raise" (fail loudly), or "mid" (Mamdani only: the midpoint of the output universe).

Fuzzy classification

fuzzytool.classify

Fuzzy rule-based classification.

The Mamdani/TSK engines answer with a number. A classifier answers with a label, and the natural fuzzy form of that is a rule base whose consequents are class labels, each rule carrying a certainty factor — how reliably that antecedent implied that class in the data it came from:

IF score is poor AND dti is high THEN class = default   (cf 0.82)

Inference is a vote: every rule adds its firing strength times its certainty to its class, and the strongest class wins. Because the vote is transparent, the classifier explains itself the same way the other engines do — see :meth:~fuzzytool.inference._common.RuleBase.explain.

>>> import numpy as np, fuzzytool as fz
>>> x = fz.Variable("x", (0, 10), terms=["lo", "hi"])
>>> clf = fz.FuzzyClassifier()
>>> _ = clf.rule(x["lo"], "a").rule(x["hi"], "b")
>>> clf(x=1.0), clf(x=9.0)
('a', 'b')
>>> clf.predict(x=np.array([1.0, 9.0])).tolist()
['a', 'b']

FuzzyClassifier

Bases: RuleBase

A fuzzy rule-based classifier (class-label consequents + certainty factors).

Parameters:

Name Type Description Default
tnorm str | Callable

t-norm for AND in antecedents (default "min").

'min'
snorm str | Callable

s-norm for OR in antecedents (default "max").

'max'
aggregation str

how a class collects the votes of its rules — "max" (the single most confident rule decides, the classic winner-takes-all of Ishibuchi's models) or "sum" (additive voting, steadier when many weak rules agree).

'max'
on_no_rule str

what to predict when nothing fires — "majority" (the most frequent class in the rule base, the default), "none" (predict None, an explicit reject option) or "raise".

'majority'
complement str | Callable

connective for NOT in antecedents (default "standard").

'standard'

classes_ property

classes_: list

The distinct class labels the rule base can predict, sorted.

rule

rule(antecedent, consequent, weight: float = 1.0) -> FuzzyClassifier

Add IF antecedent THEN class = consequent with certainty weight.

__call__

__call__(**inputs)

Classify one sample, returning the winning class label.

predict

predict(**inputs) -> np.ndarray

Classify n samples given one array per input variable.

predict_proba

predict_proba(**inputs) -> np.ndarray

Class scores normalized to sum to 1, shape (n_samples, n_classes).

These are confidence shares of the fuzzy vote, not calibrated probabilities; samples where nothing fires get a uniform row.

score

score(y_true, **inputs) -> float

Accuracy of :meth:predict against y_true.

Rule text syntax (DSL)

fuzzytool.dsl

A small text syntax for fuzzy rules.

Operator overloading (score["poor"] | dti["high"]) is the natural way to write a rule in Python. It is not the natural way to write one in a config file, a spreadsheet column, a web form, or an LLM's reply. This module parses the form everybody already writes on paper:

IF score IS poor OR dti IS high THEN premium IS high
IF x IS small AND NOT (y IS large) THEN out = 3.5 WITH 0.8
IF x IS small THEN out = 1.5 + 2*x - 0.5*y

Grammar, in short: IF THEN [WITH ]. The expression combines <variable> IS [NOT] <term> atoms with AND, OR, NOT and parentheses (&, |, ~ also work); keywords are case-insensitive and term names may be quoted if they contain spaces. The consequent is either <variable> IS <term> (Mamdani), <name> = <number> or <name> = <affine expression> (TSK), or <name> = <label> (classifier).

Every engine exposes this as :meth:~fuzzytool.inference._common.RuleBase.rule_from_text.

>>> import fuzzytool as fz
>>> score = fz.Variable("score", (300, 850), terms=["poor", "good"])
>>> premium = fz.Variable("premium", (0, 12), terms=["low", "high"])
>>> sys = fz.Mamdani()
>>> _ = sys.rule_from_text("IF score IS poor THEN premium IS high",
...                        [score, premium])
>>> sys.rules[0]
IF score is poor THEN premium is high

parse_rule

parse_rule(text: str, variables: object) -> tuple

Parse one rule into (antecedent, consequent, weight).

Parameters:

Name Type Description Default
text str

the rule, e.g. "IF x IS small AND y IS large THEN z IS high".

required
variables object

the :class:~fuzzytool.sets.Variable objects the rule may reference — a sequence or a {name: Variable} mapping.

required

Returns:

Type Description
tuple

A tuple ready to splat into any engine's rule method.

import fuzzytool as fz from fuzzytool.dsl import parse_rule x = fz.Variable("x", (0, 10), terms=["small", "large"]) ant, cons, w = parse_rule("IF NOT x IS small THEN out = 2 + 3*x WITH 0.5", [x]) ant, cons, w ((not x is small), {'const': 2.0, 'x': 3.0}, 0.5)

Rule learning (Wang-Mendel, Chi, Chiu, c-means)

fuzzytool.learn

Learning fuzzy rule bases from data.

Two families live here, matching the two ways a rule base is usually obtained:

Grid partition — the input space is carved up in advance into linguistic terms, and the data decides which consequent each cell gets.

  • :func:wang_mendel (1992) turns each training sample into one Mamdani rule, picking the highest-membership term per variable and resolving conflicts by rule degree.
  • :func:chi (1996) is its classification counterpart: same antecedents, class labels as consequents, and a certainty factor per rule.

Scatter partition — clustering finds where the data actually is, and each cluster becomes one rule, so the rule count does not explode with the number of inputs.

  • :func:subtractive_clustering (Chiu, 1994) locates cluster centers without being told how many there are.
  • :func:chiu_tsk builds a Takagi-Sugeno system from those centers, fitting the consequents by least squares.
  • :func:cmeans_tsk does the same starting from a fuzzy c-means partition.

wang_mendel

wang_mendel(X: ndarray, y: ndarray, inputs: list[Variable], output: Variable, use_weights: bool = False) -> Mamdani

Generate a Mamdani rule base from data with the Wang-Mendel method.

Parameters:

Name Type Description Default
X ndarray

inputs, shape (n_samples, len(inputs)).

required
y ndarray

targets, shape (n_samples,).

required
inputs list[Variable]

the input linguistic variables (each pre-populated with terms); column j of X maps to inputs[j].

required
output Variable

the output linguistic variable (pre-populated with terms).

required
use_weights bool

give each rule a weight equal to its (normalized) degree, so rules backed by strongly-matching samples dominate rules scraped from the edge of a partition. Off by default, matching the original method.

False

Returns:

Name Type Description
A Mamdani

class:~fuzzytool.inference.Mamdani with one rule per distinct

Mamdani

antecedent (conflicts resolved by rule degree).

chi

chi(X: ndarray, y: ndarray, inputs: list[Variable], min_certainty: float = 0.0) -> FuzzyClassifier

Generate a fuzzy classifier from labelled data (Chi, Yuen & Pedrycz, 1996).

Each sample votes, with degree equal to the product of its best-matching memberships, for the pair (antecedent cell, class). A cell's rule predicts the class with the most votes and carries the standard certainty factor (votes_winner - votes_rest) / votes_total — near 1 where a cell is pure, near 0 where classes overlap, so ambiguous regions cannot outvote clean ones.

Parameters:

Name Type Description Default
X ndarray

inputs, shape (n_samples, len(inputs)).

required
y ndarray

class labels, shape (n_samples,) (any hashable type).

required
inputs list[Variable]

input linguistic variables, pre-populated with terms.

required
min_certainty float

drop rules whose certainty factor falls below this.

0.0

Returns:

Name Type Description
A FuzzyClassifier

class:~fuzzytool.classify.FuzzyClassifier.

import numpy as np, fuzzytool as fz from fuzzytool.learn import chi rng = np.random.default_rng(0) X = rng.uniform(0, 10, size=(200, 1)) y = np.where(X[:, 0] < 5, "low", "high") v = fz.Variable("x", (0, 10), terms=["a", "b", "c", "d"]) clf = chi(X, y, [v]) str(clf(x=1.0)), str(clf(x=9.0)) ('low', 'high')

subtractive_clustering

subtractive_clustering(X: ndarray, radii: float | ndarray = 0.5, squash: float = 1.25, accept_ratio: float = 0.5, reject_ratio: float = 0.15, max_clusters: int = 50) -> np.ndarray

Chiu's subtractive clustering: find cluster centers without fixing their number.

Every data point is a candidate center, scored by how many neighbours fall within radii. The best-scoring point becomes a center, its neighbourhood is subtracted from the scores, and the process repeats until no candidate is dense enough. Unlike c-means it needs no c and no initialization, which is why it is the usual front end for building a Takagi-Sugeno rule base.

Parameters:

Name Type Description Default
X ndarray

data, shape (n_samples, n_features).

required
radii float | ndarray

neighbourhood radius per feature, as a fraction of that feature's range (scalar or one value per feature). Smaller radii -> more, tighter clusters.

0.5
squash float

the subtraction radius as a multiple of radii; > 1 keeps new centers away from the ones already found.

1.25
accept_ratio float

potentials above this fraction of the first center's are accepted outright.

0.5
reject_ratio float

potentials below this fraction are rejected outright; in between, a center is accepted only if it is far enough away.

0.15
max_clusters int

hard cap on the number of centers.

50

Returns:

Type Description
ndarray

Centers in the original scale, shape (n_clusters, n_features).

import numpy as np from fuzzytool.learn import subtractive_clustering X = np.vstack([np.zeros((20, 2)) + 1.0, np.zeros((20, 2)) + 9.0]) subtractive_clustering(X, radii=0.3).shape (2, 2)

chiu_tsk

chiu_tsk(X: ndarray, y: ndarray, radii: float | ndarray = 0.5, names: list[str] | None = None, order: int = 1, ridge: float = 0.0, **kwargs: object) -> TSK

Build a Takagi-Sugeno system from subtractive clustering (Chiu, 1994).

One rule per cluster: the antecedent is a Gaussian around the center in each input, the consequent is fitted by least squares. The rule count follows the structure of the data rather than the number of inputs, so this scales where a grid partition (n_mf ** p rules) does not.

Parameters:

Name Type Description Default
X ndarray

inputs, shape (n_samples, n_features).

required
y ndarray

targets, shape (n_samples,).

required
radii float | ndarray

neighbourhood radius per feature (see :func:subtractive_clustering).

0.5
names list[str] | None

input variable names (default x0, x1, ...).

None
order int

1 for affine consequents (first-order), 0 for constants.

1
ridge float

Tikhonov regularization for the least-squares fit; use a small value when clusters overlap and the design matrix is ill-conditioned.

0.0
**kwargs object

forwarded to :func:subtractive_clustering.

{}

Returns:

Name Type Description
A TSK

class:~fuzzytool.inference.TSK whose generated input variables are

TSK

available as system.variables_.

import numpy as np from fuzzytool.learn import chiu_tsk X = np.linspace(0, 10, 100).reshape(-1, 1) y = np.sin(X[:, 0]) sys = chiu_tsk(X, y, radii=0.25, names=["x"]) bool(abs(sys(x=3.0) - np.sin(3.0)) < 0.15) True

cmeans_tsk

cmeans_tsk(X: ndarray, y: ndarray, c: int, m: float = 2.0, names: list[str] | None = None, order: int = 1, ridge: float = 0.0, seed: int | None = 0) -> TSK

Build a Takagi-Sugeno system from a fuzzy c-means partition.

The same scatter-partition idea as :func:chiu_tsk, but the clusters come from :func:~fuzzytool.cluster.fuzzy_cmeans — so you choose the number of rules directly, and each rule's spread is the fuzzy standard deviation of its cluster along each input.

Parameters:

Name Type Description Default
X ndarray

inputs, shape (n_samples, n_features).

required
y ndarray

targets, shape (n_samples,).

required
c int

number of clusters, i.e. number of rules.

required
m float

fuzziness exponent for c-means.

2.0
names list[str] | None

input variable names (default x0, x1, ...).

None
order int

1 for affine consequents, 0 for constants.

1
ridge float

Tikhonov regularization for the least-squares fit.

0.0
seed int | None

RNG seed for the c-means initialization.

0

import numpy as np from fuzzytool.learn import cmeans_tsk X = np.linspace(0, 10, 120).reshape(-1, 1) sys = cmeans_tsk(X, np.sin(X[:, 0]), c=6, names=["x"]) bool(abs(sys(x=3.0) - np.sin(3.0)) < 0.15) True

Rule-base auditing and pruning

fuzzytool.audit

Auditing, pruning and interpretability metrics for a rule base.

A fuzzy system is supposed to be readable, but nothing stops a hand-written (or machine-learned) rule base from contradicting itself, repeating rules, leaving holes where nothing fires, or carrying terms so similar that no human can tell them apart. Those defects are invisible at the call site: the system still returns a number.

:func:audit looks for them and reports what it finds; :func:prune removes the rules that carry no information; :func:interpretability reduces the rule base to the handful of numbers usually quoted when comparing accuracy against readability.

Run against the credit-risk demo, it immediately finds something a reader would not: the rule base leaves a hole for mid-range scores with low leverage, and one term is never used.

>>> from fuzzytool import datasets
>>> from fuzzytool.audit import audit
>>> system, *_ = datasets.credit_risk()
>>> report = audit(system)
>>> report.contradictions, report.duplicates
([], [])
>>> report.coverage < 1.0 and report.unused_terms
[('dti', 'low')]
>>> next(iter(report.uncovered))
{'score': 630.0, 'dti': 0.0}

AuditReport dataclass

What :func:audit found in a rule base.

Attributes:

Name Type Description
n_rules int

number of rules.

contradictions list[tuple[int, int]]

pairs (i, j) of rules with the same antecedent but different consequents — for the same inputs the system is pulled two ways at once.

duplicates list[tuple[int, int]]

pairs (i, j) of rules identical in antecedent and consequent; the later one adds nothing.

never_fired list[int]

indices of rules whose firing strength stayed at 0 over the whole sampled input space — dead rules.

unused_terms list[tuple[str, str]]

(variable, term) pairs no rule ever mentions.

indistinguishable list[tuple[str, str, str, float]]

(variable, term_a, term_b, similarity) for term pairs that overlap more than similarity_threshold; they cannot be told apart in a linguistic reading.

uncovered list[dict]

sampled input points where no rule fires at all.

coverage float

fraction of sampled input points where some rule fires.

partition_gaps list[tuple[str, float]]

(variable, x) points of an input universe where every term of that variable is (near) zero — a hole in the partition.

ok property

ok: bool

True when the audit found nothing worth reporting.

audit

audit(system: object, n_per_axis: int = 11, max_points: int = 4096, similarity_threshold: float = 0.8, eps: float = 1e-09) -> AuditReport

Inspect a rule base for contradictions, redundancy and coverage holes.

Parameters:

Name Type Description Default
system object

any engine exposing rules (Mamdani, TSK, Tsukamoto, IT2...).

required
n_per_axis int

samples per input variable for the coverage sweep.

11
max_points int

cap on the total number of sampled input points.

4096
similarity_threshold float

Jaccard similarity above which two terms of the same variable are reported as indistinguishable.

0.8
eps float

firing strength below which a rule counts as not firing.

1e-09

Returns:

Name Type Description
An AuditReport

class:AuditReport.

interpretability

interpretability(system: object) -> dict

Readability metrics for a rule base.

Returns n_rules, n_conditions (total antecedent atoms), mean_rule_length, max_rule_length, n_input_variables, n_terms (summed over the input variables) and coverage. Fewer, shorter rules over fewer terms read better; report these next to accuracy when comparing a learned rule base against a hand-written one.

from fuzzytool import datasets from fuzzytool.audit import interpretability system, *_ = datasets.credit_risk() interpretability(system)["n_rules"] 3

prune

prune(system: object, drop_duplicates: bool = True, drop_never_fired: bool = False, min_weight: float = 0.0, **audit_kwargs: object) -> object

Return a copy of system without the rules that carry no information.

Parameters:

Name Type Description Default
system object

the engine to prune (Mamdani, TSK, Tsukamoto or an IT2 engine).

required
drop_duplicates bool

remove rules repeating an earlier rule verbatim.

True
drop_never_fired bool

remove rules that never fire anywhere on the sampled input space. Off by default: with a coarse sweep a narrow but valid rule can look dead, so switch it on deliberately.

False
min_weight float

drop rules whose weight is at or below this value.

0.0
**audit_kwargs object

forwarded to :func:audit for the firing sweep.

{}

Returns:

Type Description
object

A new system of the same class, configured identically, holding the

object

surviving rules in their original order.

import fuzzytool as fz from fuzzytool.audit import prune x = fz.Variable("x", (0, 10), terms=["lo", "hi"]) y = fz.Variable("y", (0, 10), terms=["lo", "hi"]) sys = fz.Mamdani().rule(x["lo"], y["hi"]).rule(x["lo"], y["hi"]) len(prune(sys).rules) 1

Measures on fuzzy sets

fuzzytool.measures

Measures on fuzzy sets: descriptors, distances, similarity, entropy.

Everything here works on sampled membership degrees — a 1-D array of values in [0, 1] over a shared universe, which is exactly what variable.terms[name](variable.universe) gives you (or :func:sample, which accepts either a membership function or an array).

These measures are the building blocks for comparing fuzzy sets, and the rule base auditing in :mod:fuzzytool.audit uses them to decide when two terms or two rules say the same thing.

>>> import numpy as np, fuzzytool as fz
>>> from fuzzytool import measures as ms
>>> x = np.linspace(0, 10, 501)
>>> a, b = ms.sample(fz.tri(2, 4, 6), x), ms.sample(fz.tri(3, 5, 7), x)
>>> round(ms.jaccard(a, b), 3)
0.391

sample

sample(obj, x=None) -> np.ndarray

Membership degrees as an array, from a callable MF or an array.

IT2 sets collapse to the midpoint of their footprint, so every measure here applies to type-1 and interval type-2 terms alike.

height

height(a) -> float

The largest membership degree attained by the set.

is_normal

is_normal(a, tol: float = 1e-09) -> bool

Whether the set reaches membership 1 somewhere (is normal).

support

support(x, a) -> tuple[float, float]

Bounds (lo, hi) of the support — where membership is strictly positive.

Returns (nan, nan) for the empty set.

core

core(x, a, tol: float = 1e-09) -> tuple[float, float]

Bounds (lo, hi) of the core — where membership equals 1.

alpha_cut

alpha_cut(x, a, alpha: float) -> tuple[float, float]

Bounds (lo, hi) of the alpha-cut {x : mu(x) >= alpha}.

import numpy as np, fuzzytool as fz from fuzzytool import measures as ms x = np.linspace(0, 10, 1001) lo, hi = ms.alpha_cut(x, ms.sample(fz.tri(2, 4, 6), x), 0.5) round(lo, 2), round(hi, 2) (3.0, 5.0)

cardinality

cardinality(a) -> float

Sigma-count: the sum of all membership degrees.

relative_cardinality

relative_cardinality(a) -> float

Sigma-count divided by the number of samples (a value in [0, 1]).

hamming

hamming(a, b, normalized: bool = False) -> float

Hamming distance Σ |a - b| (mean absolute difference if normalized).

euclidean

euclidean(a, b, normalized: bool = False) -> float

Euclidean distance sqrt(Σ (a - b)^2) (RMS difference if normalized).

minkowski

minkowski(a, b, p: float = 2.0, normalized: bool = False) -> float

Minkowski distance of order p (p = 1 Hamming, p = 2 Euclidean).

jaccard

jaccard(a, b) -> float

Jaccard similarity |A ∩ B| / |A ∪ B| with min/max intersection-union.

1 for identical sets, 0 for sets with disjoint supports.

dice

dice(a, b) -> float

Dice similarity 2|A ∩ B| / (|A| + |B|).

subsethood

subsethood(a, b) -> float

Kosko's subsethood: the degree to which A is contained in B.

|A ∩ B| / |A|, so it is 1 exactly when a <= b everywhere.

consistency

consistency(a, b) -> float

Consistency (possibility) of two sets: max_x min(a(x), b(x)).

0 means the two sets cannot both hold — the basis for detecting rules whose antecedents can never fire together.

fuzzy_entropy

fuzzy_entropy(a) -> float

De Luca-Termini entropy, normalized to [0, 1].

0 for a crisp set (every degree 0 or 1), 1 when every degree is 0.5 — the point of maximum vagueness.

index_of_fuzziness

index_of_fuzziness(a, kind: str = 'linear') -> float

Kaufmann's index of fuzziness: normalized distance to the nearest crisp set.

kind is "linear" (Hamming) or "quadratic" (Euclidean). 0 for a crisp set, 1 when every degree is 0.5.

Fuzzy relations

fuzzytool.relations

Fuzzy relations and the compositional rule of inference.

A fuzzy relation on X x Y is a fuzzy set of pairs: a matrix R where R[i, j] grades how strongly x_i relates to y_j. Relations are the other half of classical fuzzy logic — the half that the rule-based engines in :mod:fuzzytool.inference hide behind an efficient shortcut.

This module makes that half explicit and lets you work with it directly:

  • build a relation from two fuzzy sets (:func:cartesian) or from an implication (:func:implication_relation);
  • combine relations (:func:compose, max-min by default);
  • apply one to a fuzzy set — the compositional rule of inference (:func:cri), which is what "IF x is A THEN y is B" means before any engine optimizes it away;
  • project, extend, invert and close relations.

Everything is plain NumPy, and every operation takes the connectives by name, so a relation composed with the product t-norm is one argument away.

>>> import numpy as np, fuzzytool as fz
>>> from fuzzytool import relations as rel
>>> x = np.linspace(0, 10, 101)
>>> a, b = fz.tri(2, 4, 6)(x), fz.tri(5, 7, 9)(x)
>>> r = rel.cartesian(a, b)                 # "IF x is A THEN y is B"
>>> out = rel.cri(a, r)                     # feeding A back in recovers B
>>> bool(np.allclose(out, b))
True

cartesian

cartesian(a, b, tnorm='min') -> np.ndarray

Cartesian product A x B: the relation R[i, j] = T(a_i, b_j).

This is Mamdani's reading of a fuzzy rule — the relation induced by "IF x is A THEN y is B".

compose

compose(r, s, tnorm='min', snorm='max') -> np.ndarray

Sup-t composition of relations R ∘ S (max-min by default).

R is (n, m) on X x Y and S is (m, k) on Y x Z; the result is (n, k) on X x Z, aggregating over the shared Y.

cri

cri(a, r, tnorm='min', snorm='max') -> np.ndarray

Compositional rule of inference: B' = A' ∘ R.

Given an observed fuzzy input A' and a relation R, produce the induced fuzzy output. With R = cartesian(A, B) and A' = A this returns B — the fuzzy-relational statement of modus ponens.

projection

projection(r, axis: int = 1, snorm='max') -> np.ndarray

Project a relation onto one of its universes (axis=1 keeps X).

The projection of R onto X is sup_y R(x, y): the shadow the relation casts on that axis.

cylindrical_extension

cylindrical_extension(a, n: int, axis: int = 1) -> np.ndarray

Extend a fuzzy set to a relation that ignores the other universe.

With axis=1 the set indexes the rows and is repeated across n columns. This is the inverse of :func:projection and the standard way to make two sets defined on different universes comparable.

inverse

inverse(r) -> np.ndarray

The converse relation R^-1[j, i] = R[i, j].

union

union(r, s, snorm='max') -> np.ndarray

Elementwise union of two relations on the same universes.

intersection

intersection(r, s, tnorm='min') -> np.ndarray

Elementwise intersection of two relations on the same universes.

implication_relation

implication_relation(a: ndarray, b: ndarray, kind: str = 'mamdani') -> np.ndarray

The relation induced by "IF x is A THEN y is B" under a given implication.

Parameters:

Name Type Description Default
a ndarray

antecedent membership over X.

required
b ndarray

consequent membership over Y.

required
kind str

"mamdani" (min), "larsen" (product), "lukasiewicz" (min(1, 1 - a + b)), "godel" (1 if a <= b else b), "kleene_dienes" (max(1 - a, b)) or "zadeh" (max(min(a, b), 1 - a)).

'mamdani'

The first two are the conjunctive readings the Mamdani engine uses; the rest are genuine implications, which behave differently when the antecedent does not fire.

transitive_closure

transitive_closure(r, tnorm='min', snorm='max', max_iter: int = 100) -> np.ndarray

Max-min (or sup-t) transitive closure of a square relation.

Repeatedly unions R with R ∘ R until nothing changes. Applied to a fuzzy proximity relation (reflexive and symmetric) this yields a fuzzy equivalence relation, whose alpha-cuts are a hierarchy of crisp partitions — relational fuzzy clustering in three lines.

is_reflexive

is_reflexive(r, tol: float = 1e-09) -> bool

Whether R(x, x) == 1 for every x.

is_symmetric

is_symmetric(r, tol: float = 1e-09) -> bool

Whether R(x, y) == R(y, x) everywhere.

is_transitive

is_transitive(r, tnorm='min', snorm='max', tol: float = 1e-09) -> bool

Whether R ∘ R <= R (sup-t transitivity).

Fuzzy numbers

fuzzytool.fuzzynum

Fuzzy numbers and their arithmetic.

A fuzzy number is a convex, normal fuzzy set on the reals. This module provides the two most used shapes — triangular (TFN) and trapezoidal (TrFN) — with standard fuzzy arithmetic (+ - * / and scalar scaling), alpha-cuts, a crisp (centroid) value, and a vertex distance used by fuzzy MCDM (see :mod:fuzzytool.mcdm).

Multiplication and division use the common positive-support approximation (operate on the ordered defining points), which is exact for +/- and a good approximation for *// when supports are positive.

FuzzyNumber

Base class for fuzzy numbers defined by ordered points p.

distance

distance(other: FuzzyNumber) -> float

Vertex distance to another fuzzy number of the same shape.

TriangularFuzzyNumber

Bases: FuzzyNumber

Triangular fuzzy number (a, b, c) with peak at b.

TrapezoidalFuzzyNumber

Bases: FuzzyNumber

Trapezoidal fuzzy number (a, b, c, d) with flat top [b, c].

tfn

tfn(a: float, b: float, c: float) -> TriangularFuzzyNumber

Shortcut for :class:TriangularFuzzyNumber.

trfn

trfn(a: float, b: float, c: float, d: float) -> TrapezoidalFuzzyNumber

Shortcut for :class:TrapezoidalFuzzyNumber.

rank

rank(numbers: list[FuzzyNumber]) -> list[int]

Indices that sort numbers from largest to smallest by centroid.

Fuzzy MCDM

fuzzytool.mcdm

Fuzzy multi-criteria decision making (MCDM).

  • :func:fuzzy_topsis — Chen's (2000) fuzzy TOPSIS: rank alternatives by their closeness to the fuzzy positive-ideal solution.
  • :func:fuzzy_ahp — Chang's (1996) extent-analysis AHP: derive crisp criterion weights from a triangular-fuzzy pairwise-comparison matrix.

Inputs are triangular fuzzy numbers (:class:~fuzzytool.fuzzynum.TriangularFuzzyNumber).

TopsisResult dataclass

Result of :func:fuzzy_topsis.

Attributes:

Name Type Description
closeness ndarray

closeness coefficient CC per alternative (higher = better).

ranking list[int]

alternative indices ordered best-to-worst.

d_plus ndarray

distance to the fuzzy positive-ideal solution per alternative.

d_minus ndarray

distance to the fuzzy negative-ideal solution per alternative.

fuzzy_topsis

fuzzy_topsis(matrix: list, weights: list, benefit: list) -> TopsisResult

Rank alternatives with Chen's fuzzy TOPSIS.

Parameters:

Name Type Description Default
matrix list

m x n ratings as TriangularFuzzyNumber (m alternatives, n criteria).

required
weights list

n criterion weights as TriangularFuzzyNumber.

required
benefit list

length-n booleans — True if a criterion is to be maximized (benefit), False if minimized (cost).

required

Returns:

Name Type Description
A TopsisResult

class:TopsisResult.

fuzzy_ahp

fuzzy_ahp(matrix: list) -> np.ndarray

Crisp criterion weights from a fuzzy pairwise matrix (Chang's method).

Parameters:

Name Type Description Default
matrix list

n x n pairwise comparisons as TriangularFuzzyNumber; the diagonal is (1, 1, 1) and matrix[j][i] is the reciprocal of matrix[i][j].

required

Returns:

Type Description
ndarray

A length-n array of normalized, non-negative weights summing to 1.

Serialization

fuzzytool.serialize

Save and load fuzzy inference systems as JSON.

Every rule-based engine round-trips: :class:~fuzzytool.inference.Mamdani, :class:~fuzzytool.inference.TSK, :class:~fuzzytool.inference.Tsukamoto, :class:~fuzzytool.classify.FuzzyClassifier and the interval type-2 engines. The requirements are that the connectives/defuzzifier were given by name (a bare callable has no portable representation) and that every membership function is registered — the built-ins are, and custom shapes join them through :func:fuzzytool.membership.register.

fz.save(system, "system.json")
system = fz.load("system.json")

The file records a format_version; a file written by a newer, incompatible version of fuzzytool is rejected with a clear error rather than silently misread.

to_dict

to_dict(system) -> dict

Serialize a rule-based system to a JSON-ready dict.

from_dict

from_dict(d: dict)

Rebuild a system from :func:to_dict output.

save

save(system, path: str) -> None

Write system to path as JSON.

load

load(path: str)

Read a system back from a JSON file written by :func:save.

Interval type-2 — sets

fuzzytool.type2.sets

Interval type-2 (IT2) membership functions.

An :class:IntervalType2MF carries a lower (LMF) and an upper (UMF) type-1 membership function. Calling it returns the membership interval (lower, upper); the engines also call .lower(x) / .upper(x) directly.

The constructors cover the standard ways of building an FOU from a type-1 set:

  • :func:it2 — explicit LMF/UMF;
  • :func:it2_scale — height uncertainty (LMF is a scaled-down UMF);
  • :func:it2_gauss_uncertain_mean — a Gaussian with mean in [c1, c2];
  • :func:it2_gauss_uncertain_std — a Gaussian with uncertain spread.

IntervalType2MF

An IT2 set defined by a lower (LMF) and an upper (UMF) type-1 MF.

The UMF must dominate the LMF everywhere (LMF(x) <= UMF(x)); this is not enforced at construction (it would require sampling the universe) but holds for every set built through the constructors below.

it2

it2(lower: MembershipFunction, upper: MembershipFunction) -> IntervalType2MF

An IT2 set from explicit lower and upper type-1 membership functions.

it2_scale

it2_scale(mf: MembershipFunction, scale: float) -> IntervalType2MF

Height-uncertainty FOU: UMF is mf, LMF is scale * mf (0 < scale ≤ 1).

it2_gauss_uncertain_mean

it2_gauss_uncertain_mean(c1: float, c2: float, sigma: float) -> IntervalType2MF

Gaussian with an uncertain mean spanning [c1, c2] (c1 < c2).

The UMF is the upper envelope of all Gaussians with mean in [c1, c2] (flat-topped at 1 between the means); the LMF is their lower envelope.

it2_gauss_uncertain_std

it2_gauss_uncertain_std(c: float, sigma_lo: float, sigma_hi: float) -> IntervalType2MF

Gaussian with an uncertain spread: UMF uses the wider σ, LMF the narrower.

Interval type-2 — inference

fuzzytool.type2.inference

Interval type-2 inference engines.

Both engines evaluate each rule's antecedent to a firing interval [f_low, f_high] (via :meth:Antecedent.eval_interval) and then collapse the rule base with type reduction, returning the midpoint of the type-reduced interval [y_l, y_r].

  • :class:IT2Mamdani uses center-of-sets type reduction: each consequent IT2 set is summarized by its centroid interval (computed once and cached), and the reducer combines those centroids weighted by the firing intervals.
  • :class:IT2TSK has crisp consequents (numbers, coefficient mappings, or callables) and type-reduces them directly.

The reducer is pluggable (reducer="km" | "eiasc" | "nie-tan", or any callable) exactly like the type-1 defuzzifiers, and both engines share the type-1 engines' predict / on_no_rule / explain contract.

IT2Mamdani

Bases: _IT2Base

Interval type-2 Mamdani inference (center-of-sets type reduction).

Parameters:

Name Type Description Default
tnorm str | Callable

t-norm for AND in antecedents (default "min").

'min'
snorm str | Callable

s-norm for OR in antecedents (default "max").

'max'
reducer str | Callable

type reducer — "km" (default), "eiasc" (same result, faster) or "nie-tan" (closed-form approximation).

'km'
on_no_rule str

what to return when no rule fires — "mid" (midpoint of the output universe, the default), "nan" or "raise".

'mid'
complement str | Callable

connective for NOT in antecedents (default "standard").

'standard'

rule

rule(antecedent: Antecedent, consequent: Proposition, weight: float = 1.0) -> IT2Mamdani

Add IF antecedent THEN output is term and return self.

__call__

__call__(**inputs: float)

Run inference. Returns a float for one output, else a dict by name.

predict

predict(**inputs)

Vectorized inference over array-valued inputs (n per keyword).

The antecedents are evaluated in batch; type reduction is inherently per-sample, so it still loops — but each membership function is touched once for the whole batch instead of once per sample.

IT2TSK

Bases: _IT2Base

Interval type-2 Takagi-Sugeno inference (type reduction over crisp consequents).

Consequents follow the same convention as the type-1 :class:~fuzzytool.inference.tsk.TSK engine: a number, a coefficient mapping {"const": b0, "x": b1, ...}, or a callable f(**inputs) -> float.

Parameters:

Name Type Description Default
tnorm str | Callable

t-norm for AND in antecedents (default "min").

'min'
snorm str | Callable

s-norm for OR in antecedents (default "max").

'max'
reducer str | Callable

type reducer — "km", "eiasc" or "nie-tan".

'km'
on_no_rule str

"nan" (default) or "raise".

'nan'
complement str | Callable

connective for NOT in antecedents (default "standard").

'standard'

rule

rule(antecedent: Antecedent, consequent, weight: float = 1.0) -> IT2TSK

Add IF antecedent THEN output = consequent and return self.

__call__

__call__(**inputs: float) -> float

Run inference, returning the midpoint of the type-reduced interval.

predict

predict(**inputs) -> np.ndarray

Vectorized inference over array-valued inputs (n per keyword).

Interval type-2 — type reduction

fuzzytool.type2.reduction

Karnik-Mendel type reduction.

Type reduction turns the interval-valued output of an IT2 system into a crisp interval [y_l, y_r] (typically defuzzified as its midpoint). The Karnik-Mendel (KM) algorithm finds each endpoint by locating the switch point where the per-point weight flips between its lower and upper bound.

The single primitive :func:km_endpoint is reused everywhere: for an IT2 set's centroid (points = universe samples, weights = the FOU) and for an IT2 rule base (points = consequent centroids, weights = rule firing intervals).

km_endpoint

km_endpoint(points: ndarray, lower: ndarray, upper: ndarray, side: str = 'l', max_iter: int = 100) -> float

One endpoint of the type-reduced interval via Karnik-Mendel.

Parameters:

Name Type Description Default
points ndarray

the y-values (e.g. consequent centroids or universe samples).

required
lower ndarray

per-point weight lower bounds (same shape as points).

required
upper ndarray

per-point weight upper bounds (same shape as points).

required
side str

"l" for the left endpoint y_l, "r" for the right y_r.

'l'
max_iter int

safeguard on the Karnik-Mendel iteration count.

100

Left endpoint uses the upper weights to the left of the switch and the lower weights to its right; the right endpoint mirrors this.

karnik_mendel

karnik_mendel(points, lower, upper) -> tuple[float, float]

Type-reduce to the interval (y_l, y_r) over a shared set of points.

eiasc

eiasc(points, lower, upper) -> tuple[float, float]

Type-reduce with EIASC (Wu & Nie, 2011).

The Enhanced Iterative Algorithm with Stop Condition computes the very same interval as :func:karnik_mendel but walks the sorted points from one end and stops at the switch point, so it needs no repeated full re-averaging. In practice it is the fastest exact type reducer for rule bases of the size fuzzy systems actually use.

import numpy as np from fuzzytool.type2 import eiasc, karnik_mendel pts, lo, hi = np.array([1., 3., 6.]), np.array([.2, .4, .1]), np.array([.6, .9, .5]) np.allclose(eiasc(pts, lo, hi), karnik_mendel(pts, lo, hi)) True

nie_tan

nie_tan(points, lower, upper) -> tuple[float, float]

Nie-Tan type reduction: the centroid of the FOU's average membership.

A closed-form approximation — one pass, no iteration — that returns a degenerate interval (y, y). Accurate enough for control loops where the exact switch points do not matter, and typically an order of magnitude cheaper than Karnik-Mendel.

get_type_reducer

get_type_reducer(name: str | Callable) -> Callable

Resolve a type reducer by name (or pass a callable through unchanged).

A reducer maps (points, lower, upper) to the interval (y_l, y_r).

centroid_it2

centroid_it2(it2mf, universe, reducer=karnik_mendel) -> tuple[float, float]

Centroid interval (c_l, c_r) of an IT2 set over universe.

A plain type-1 membership function collapses to a degenerate footprint (lower == upper), so type-1 and IT2 consequents mix freely in the same rule base — just as they already do in antecedents.

General type-2 (zSlices)

fuzzytool.type2.general

General type-2 (GT2) fuzzy sets via the zSlices / alpha-plane representation.

An interval type-2 set treats every point inside its footprint of uncertainty (FOU) as equally possible. A general type-2 set adds a secondary membership that weights those possibilities — the third dimension IT2 throws away.

A practical, exact way to handle that third dimension is the zSlices (alpha -plane) decomposition (Wagner & Hagras): slice the secondary domain at levels z and, at each level, the GT2 set reduces to an ordinary IT2 set whose FOU is the z-cut. Inference and type reduction then run the existing IT2 machinery on each slice and combine the results, weighted by z.

Here a GT2 set is built from an IT2 footprint plus a triangular secondary peaking at the FOU's principal (mid) MF: at z -> 0 a slice is the full FOU, at z = 1 it collapses to the principal MF.

  • :class:GeneralType2MF — a stack of IT2 z-slices (also exposes the overall FOU via lower/upper, so it degrades gracefully to IT2).
  • :func:gt2_from_it2, :func:gt2_gauss_uncertain_mean, :func:gt2_scale — constructors.
  • :class:GeneralType2Mamdani — z-weighted stack of IT2 Mamdani inferences.
  • :func:centroid_gt2 — zSlices centroid of a single GT2 set.

GeneralType2MF

A general type-2 set represented as a stack of IT2 z-slices.

Parameters:

Name Type Description Default
slices list[IntervalType2MF]

IT2 sets, one per z level, narrowing toward the principal MF as z increases.

required
zlevels ndarray

the secondary levels (in (0, 1]), ascending, aligned with slices.

required
footprint IntervalType2MF

the overall FOU as an IT2 set (used for lower/upper so the GT2 set can stand in for its IT2 footprint).

required

slice

slice(i: int) -> IntervalType2MF

The IT2 set at z-level index i.

GeneralType2Mamdani

General type-2 Mamdani inference via the zSlices decomposition.

At each z level the GT2 terms collapse to their IT2 z-slice and the existing :class:~fuzzytool.type2.inference.IT2Mamdani engine (center-of-sets type reduction) produces a crisp output y(z). The final result is the z-weighted average Σ z·y(z) / Σ z — the centroid of the type-reduced general type-2 output.

Variables may mix GT2 and IT2/type-1 terms; only the GT2 terms are sliced. All GT2 terms must share the same z levels (same n_slices).

rule

rule(antecedent, consequent, weight: float = 1.0) -> GeneralType2Mamdani

Add IF antecedent THEN output is term and return self.

__call__

__call__(**inputs: float)

Run inference. Returns a float for one output, else a dict by name.

gt2_from_it2

gt2_from_it2(footprint: IntervalType2MF, n_slices: int = 5) -> GeneralType2MF

Build a GT2 set from an IT2 FOU with a triangular secondary MF.

The secondary membership peaks at the principal MF (the midline of the FOU) and falls linearly to 0 at the LMF/UMF. The z-slice at level z is the IT2 set whose FOU is narrowed from the full footprint toward that midline by a factor (1 - z).

Parameters:

Name Type Description Default
footprint IntervalType2MF

the IT2 set describing the full footprint of uncertainty.

required
n_slices int

number of z levels (z = 1/n .. 1). More slices = a finer secondary, at proportional cost.

5

gt2_gauss_uncertain_mean

gt2_gauss_uncertain_mean(c1: float, c2: float, sigma: float, n_slices: int = 5) -> GeneralType2MF

GT2 Gaussian with an uncertain mean in [c1, c2] (triangular secondary).

gt2_scale

gt2_scale(mf, scale: float, n_slices: int = 5) -> GeneralType2MF

GT2 set from a height-uncertainty FOU (UMF mf, LMF scale * mf).

centroid_gt2

centroid_gt2(gt2mf: GeneralType2MF, universe) -> float

zSlices centroid of a GT2 set: z-weighted mean of per-slice IT2 centroids.

Fuzzy clustering

fuzzytool.cluster

Fuzzy clustering.

Unlike crisp k-means, fuzzy clustering lets each sample belong to several clusters with graded membership. This module provides three algorithms and a small set of validity metrics, all on plain NumPy arrays of shape (n_samples, n_features):

  • :func:fuzzy_cmeans — Bezdek's FCM (spherical clusters, Euclidean norm).
  • :func:gustafson_kessel — GK, an adaptive norm per cluster that captures ellipsoidal shapes.
  • :func:possibilistic_cmeans — PCM, which drops the "memberships sum to 1" constraint so outliers get low typicality in every cluster.

Each returns a :class:ClusterResult. Every algorithm accepts a seed for reproducibility.

ClusterResult dataclass

Outcome of a fuzzy clustering run.

Attributes:

Name Type Description
centers ndarray

cluster prototypes, shape (c, n_features).

u ndarray

membership/typicality matrix, shape (c, n_samples).

n_iter int

iterations run until convergence.

objective float

final value of the algorithm's objective function.

labels ndarray

hard assignment argmax over u, shape (n_samples,).

fuzzy_cmeans

fuzzy_cmeans(X: ndarray, c: int, m: float = 2.0, max_iter: int = 150, tol: float = 1e-05, seed: int | None = None) -> ClusterResult

Bezdek's fuzzy c-means.

Parameters:

Name Type Description Default
X ndarray

data, shape (n_samples, n_features).

required
c int

number of clusters.

required
m float

fuzziness exponent (> 1; 2.0 is standard).

2.0
max_iter int

maximum iterations.

150
tol float

stop when the membership matrix changes by less than this (max-norm).

1e-05
seed int | None

RNG seed for the membership initialization.

None

gustafson_kessel

gustafson_kessel(X: ndarray, c: int, m: float = 2.0, max_iter: int = 150, tol: float = 1e-05, seed: int | None = None, reg: float = 1e-10) -> ClusterResult

Gustafson-Kessel clustering (adaptive per-cluster Mahalanobis norm).

Each cluster learns a covariance-shaped, unit-determinant norm, so GK fits ellipsoidal clusters that FCM (fixed spherical norm) cannot.

Parameters:

Name Type Description Default
X ndarray

data, shape (n_samples, n_features).

required
c int

number of clusters.

required
m float

fuzziness exponent (> 1).

2.0
max_iter int

maximum iterations.

150
tol float

convergence threshold on the membership matrix (max-norm).

1e-05
seed int | None

RNG seed for initialization.

None
reg float

ridge added to each fuzzy covariance for numerical stability.

1e-10

possibilistic_cmeans

possibilistic_cmeans(X: ndarray, c: int, m: float = 2.0, max_iter: int = 150, tol: float = 1e-05, seed: int | None = None, k: float = 1.0) -> ClusterResult

Possibilistic c-means (Krishnapuram-Keller).

PCM drops the probabilistic constraint that memberships sum to 1: each value is a typicality in [0, 1], so noise points score low everywhere. It is initialized from an FCM run, which also fixes each cluster's scale eta.

Parameters:

Name Type Description Default
X ndarray

data, shape (n_samples, n_features).

required
c int

number of clusters.

required
m float

fuzziness exponent (> 1).

2.0
max_iter int

maximum iterations.

150
tol float

convergence threshold on the typicality matrix (max-norm).

1e-05
seed int | None

RNG seed (passed to the initializing FCM).

None
k float

scale multiplier for the bandwidth eta (1.0 is standard).

1.0

partition_coefficient

partition_coefficient(u: ndarray) -> float

Bezdek's partition coefficient in (1/c, 1]; higher = crisper.

partition_entropy

partition_entropy(u: ndarray) -> float

Partition entropy in [0, log c); lower = crisper.

xie_beni

xie_beni(X, centers: ndarray, u: ndarray, m: float = 2.0) -> float

Xie-Beni index (compactness / separation); lower is better.

ANFIS

fuzzytool.anfis

ANFIS — an adaptive network-based fuzzy inference system (Jang, 1993).

ANFIS is a first-order Takagi-Sugeno system whose parameters are learned from data. Each rule fires with the product of its inputs' Gaussian memberships and emits an affine function of the inputs.

Training uses Jang's hybrid scheme, one pass per epoch:

  1. with the premise (Gaussian) parameters fixed, the consequent (affine) parameters are solved in closed form by least squares — the output is linear in them;
  2. with the consequents fixed, the premise centers and widths take a gradient-descent step on the mean squared error.

The rules come from one of two partitions:

  • partition="grid" — the classic layout: n_mf membership functions per input, one rule per combination, so n_mf ** p rules. Readable, but only for a handful of inputs.
  • partition="cluster" — one rule per fuzzy c-means cluster, each with its own Gaussian per input. The rule count is chosen directly (n_rules) and does not grow with the number of inputs, which is what makes ANFIS usable beyond three or four features.

Pure NumPy.

ANFIS

A trainable first-order Sugeno system.

Parameters:

Name Type Description Default
n_inputs int

number of input features p.

required
n_mf int

Gaussian membership functions per input, for the grid partition (rules = n_mf ** p). Ignored when partition="cluster".

3
learning_rate float

step size for the premise gradient updates.

0.05
partition str

"grid" (default) or "cluster".

'grid'
n_rules int

number of rules when partition="cluster".

8
ridge float | None

Tikhonov regularization added to the consequent least-squares solve. The design matrix has n_rules * (p + 1) columns and goes ill-conditioned as soon as two rules overlap. None (default) picks 1e-6 for a cluster partition — whose rules overlap by construction, and where an unregularized solve can diverge — and 0 for a grid partition, matching Jang's original scheme.

None
tol float

stop early when the training RMSE improves by less than this for patience consecutive epochs. 0 (default) runs every epoch.

0.0
patience int

how many stagnant epochs to tolerate before stopping.

5
max_rules int

guard against an accidentally enormous grid partition.

20000
seed int | None

RNG seed for the clustering initialization.

0

fit

fit(X, y, epochs: int = 100) -> ANFIS

Train on X (n, p) and targets y (n,) for epochs epochs.

predict

predict(X) -> np.ndarray

Predict outputs for X (n, p). Requires a prior :meth:fit.

to_tsk

to_tsk(names: list[str] | None = None)

Export the trained model as a plain :class:~fuzzytool.inference.TSK.

The result is an ordinary fuzzy system — inspectable, serializable, explainable — rather than a fitted estimator, so a trained ANFIS can be audited or saved like any hand-written rule base.

import numpy as np from fuzzytool import ANFIS X = np.linspace(0, 6, 60).reshape(-1, 1) model = ANFIS(1, n_mf=3).fit(X, np.sin(X[:, 0]), epochs=20) sys = model.to_tsk(["x"]) direct = model.predict(np.full((1, 1), 2.0)).item() bool(abs(sys(x=2.0) - direct) < 1e-9) True

get_params

get_params(deep: bool = True) -> dict

Hyperparameters, for scikit-learn compatibility (Pipeline/GridSearch).

set_params

set_params(**params) -> ANFIS

Set hyperparameters (resets trained state), returning self.

F-transform

fuzzytool.ftransform

F-transform — the fuzzy transform of Perfilieva.

The F-transform projects a function onto a fuzzy partition of its domain. Over a uniform partition by triangular basis functions A_1..A_n that satisfy the Ruspini condition (they sum to 1 everywhere on [a, b]):

  • the direct transform reduces the data to n components, each a membership-weighted average of the samples falling under one basis function;
  • the inverse transform reconstructs an approximation f̂(x) = Σ_k F_k · A_k(x).

With few components the round trip smooths/denoises a signal; with many it approximates it closely. Pure NumPy.

FTransform

A triangular F-transform over a uniform fuzzy partition of [a, b].

Parameters:

Name Type Description Default
a float

left end of the domain.

required
b float

right end of the domain (b > a).

required
n_basis int

number of basis functions / components (>= 2).

required

basis

basis(x) -> np.ndarray

Triangular basis matrix A of shape (len(x), n_basis).

Columns form a partition of unity on [a, b] (each row sums to 1).

direct

direct(x, y) -> np.ndarray

Direct F-transform: the n_basis components from samples (x, y).

inverse

inverse(x, components: ndarray | None = None) -> np.ndarray

Inverse F-transform: reconstruct values at x from components.

fit

fit(x, y) -> FTransform

Compute and store the direct transform of (x, y); returns self.

smooth

smooth(x) -> np.ndarray

Convenience: direct-then-inverse at x (requires a prior fit).

Visualization

fuzzytool.viz

Matplotlib visualization helpers.

Importing this module requires matplotlib (an optional dependency; install with pip install fuzzytool[viz]). Visualization is a first-class priority of the project, mirroring its sibling turboswarm.

plot_variable

plot_variable(variable: Variable, ax=None)

Plot every term's membership function over the variable's universe.

plot_it2_variable

plot_it2_variable(variable: Variable, ax=None)

Plot an IT2 variable: each term's lower/upper MF with a shaded FOU.

Type-1 terms (if any are mixed in) are drawn as a single line.

control_surface

control_surface(system, x_var: Variable, y_var: Variable, n: int = 41, ax=None)

Plot a system's output as a surface over two input variables.

system must be callable as system(**{x_var.name: x, y_var.name: y}) and return a scalar (single-output Mamdani or TSK).

plot_clusters

plot_clusters(X, result, ax=None)

Scatter 2-D data colored by hard label, with cluster centers marked.

result is a :class:~fuzzytool.cluster.ClusterResult. Point opacity encodes the top membership, so fuzzy/boundary points appear fainter.

Integrations — pandas

fuzzytool.integrations.pandas

pandas integration: DataFrame in/out and tabular views of fuzzy objects.

Install with pip install fuzzytool[pandas]. Nothing here imports pandas at module load beyond a guarded check, so the rest of the toolkit stays dependency -free.

  • :func:predict_df — batch inference straight from a DataFrame.
  • :func:rules_dataframe — a readable table of a system's rule base.
  • :func:memberships_dataframe — a clustering result's membership matrix.
  • :func:components_dataframe — an F-transform's components.

predict_df

predict_df(system: Mamdani | TSK, df: DataFrame, columns: list[str] | None = None) -> pd.Series | pd.DataFrame

Run vectorized inference over a DataFrame.

Parameters:

Name Type Description Default
system Mamdani | TSK

a :class:~fuzzytool.inference.Mamdani or :class:~fuzzytool.inference.TSK system.

required
df DataFrame

a DataFrame whose columns include the system's input variables.

required
columns list[str] | None

input column names to use, in order. Defaults to the variables the system's rules reference (see :func:~fuzzytool.integrations._util.input_variable_names).

None

Returns:

Type Description
Series | DataFrame

A Series (single output, named after the output variable) or a

Series | DataFrame

DataFrame (multiple outputs), aligned to df's index.

rules_dataframe

rules_dataframe(system)

Return the system's rule base as a DataFrame (one row per rule).

Columns: antecedent (the IF part as text), consequent (the THEN part) and weight.

memberships_dataframe

memberships_dataframe(result, index=None, prefix: str = 'cluster')

Return a clustering result's membership matrix as a DataFrame.

One row per sample, one column {prefix}_{k} per cluster, plus a label column holding the hard (argmax) assignment.

components_dataframe

components_dataframe(ftransform)

Return an :class:~fuzzytool.ftransform.FTransform's components as a DataFrame.

Columns: node (the basis-function center) and component (its value).

Integrations — scikit-learn

fuzzytool.integrations.sklearn

scikit-learn integration: fuzzy systems as estimators and transformers.

These objects follow the scikit-learn estimator protocol (fit returns self, plus predict/transform, get_params/set_params and a score/fit_transform), so they slot into a Pipeline, GridSearchCV or cross_val_score — yet, like :class:~fuzzytool.anfis.ANFIS, they do not import scikit-learn. The [sklearn] extra simply installs scikit-learn so you have those tools available:

pip install fuzzytool[sklearn]
  • :class:Fuzzifier — a transformer turning crisp features into fuzzy membership-degree features (one column per term).
  • :class:WangMendelRegressor — learns a Mamdani rule base from data (Wang-Mendel) and predicts with it.
  • :class:FuzzySystemRegressor — wraps an already-built Mamdani/TSK system so it can be scored or pipelined.
  • :class:ChiClassifier — learns an interpretable fuzzy rule-based classifier (Chi et al.), with predict_proba.
  • :class:FuzzySystemClassifier — wraps an already-built :class:~fuzzytool.classify.FuzzyClassifier.

Fuzzifier

Bases: _Estimator

Expand crisp features into fuzzy membership-degree features.

Column j of X is interpreted through variables[j]: each of that variable's terms becomes one output column holding the membership degree. The result is a soft, interpretable encoding that downstream linear or tree models can consume.

Parameters:

Name Type Description Default
variables list[Variable]

one :class:~fuzzytool.sets.Variable per input column, in column order.

required

WangMendelRegressor

Bases: _Estimator

A regressor that learns a Mamdani rule base from data (Wang-Mendel).

Parameters:

Name Type Description Default
inputs list[Variable]

input linguistic variables (pre-populated with terms), one per column of X.

required
output Variable

the output linguistic variable (pre-populated with terms).

required

FuzzySystemRegressor

Bases: _Estimator

Wrap an already-built single-output fuzzy system as an sklearn regressor.

fit does not change the system (it is already defined); it only records the input column order. Use this to cross-validate, score, or pipeline a hand-written Mamdani/TSK system.

Parameters:

Name Type Description Default
system Mamdani | TSK

a single-output Mamdani or TSK system.

required
columns list[str] | None

input variable names matching X's columns, in order. Defaults to the variables the system's rules reference.

None

ChiClassifier

Bases: _Estimator

A fuzzy rule-based classifier learned from data (Chi et al.).

Fits an interpretable rule base — one rule per occupied cell of the input partition, each with a certainty factor — and classifies with it. Unlike a black-box classifier, self.system_.summary() prints the model, and self.system_.explain(...) says which rules decided a given prediction.

Parameters:

Name Type Description Default
inputs list[Variable]

input linguistic variables (pre-populated with terms), one per column of X.

required
min_certainty float

drop learned rules below this certainty factor.

0.0
aggregation str

"max" (winner-takes-all) or "sum" (additive votes).

'max'

FuzzySystemClassifier

Bases: _Estimator

Wrap an already-built :class:~fuzzytool.classify.FuzzyClassifier as an estimator.

fit does not change the rule base; it only records the input column order, so a hand-written classifier can be cross-validated or pipelined.

Parameters:

Name Type Description Default
system object

the classifier to wrap.

required
columns list[str] | None

input variable names matching X's columns, in order. Defaults to the variables the rules reference.

None

Integrations — SciPy

fuzzytool.integrations.scipy

SciPy integration: tune a fuzzy system's membership functions to data.

Install with pip install fuzzytool[scipy]. :func:tune adjusts the parameters of a system's built-in membership functions so its output fits a dataset, using :func:scipy.optimize.least_squares. It mutates the system in place and returns SciPy's OptimizeResult (.x, .cost, .nfev, .success …) — a drop-in way to refine a hand-written Mamdani/TSK system.

Only built-in membership shapes (tri, trap, gauss, gbell, sigmoid, ramp_up, ramp_down) are tunable; custom callables are left untouched. Shape validity is preserved every iteration (breakpoints are kept ordered, widths kept positive), so the optimizer explores freely without producing degenerate sets.

tune

tune(system: Mamdani | TSK, X: ndarray, y: ndarray, columns: list[str] | None = None, tune_outputs: bool = True, **least_squares_kwargs) -> OptimizeResult

Fit a system's membership-function parameters to data.

Any extra keyword arguments are forwarded to :func:scipy.optimize.least_squares (e.g. max_nfev).

Parameters:

Name Type Description Default
system Mamdani | TSK

a Mamdani or TSK system (mutated in place with the best params).

required
X ndarray

inputs, shape (n_samples, n_inputs).

required
y ndarray

targets, shape (n_samples,).

required
columns list[str] | None

input variable names matching X's columns, in order. Defaults to the variables the rules reference.

None
tune_outputs bool

also tune the output variables' MFs (Mamdani only).

True

Returns:

Type Description
OptimizeResult

The SciPy OptimizeResult.

Integrations — turboswarm (PSO)

fuzzytool.integrations.turboswarm

turboswarm integration: tune a fuzzy system with Particle Swarm Optimization.

Install with pip install fuzzytool[turboswarm]. :func:tune fits the parameters of a system's built-in membership functions to data with turboswarm <https://pypi.org/project/turboswarm/>_'s PSO — a gradient-free, global optimizer. It is the metaheuristic sibling of :func:fuzzytool.integrations.scipy.tune (local least-squares): slower, but it escapes poor local optima and needs no derivatives, which suits the rugged, non-smooth error surfaces fuzzy rule bases often produce.

Each parameter is searched within a box of ±margin × universe around its current value; shape validity (ordered breakpoints, positive widths) is enforced every evaluation, so the swarm explores freely. The system is mutated in place with the best parameters found and the turboswarm PsoResult is returned.

tune

tune(system: Mamdani | TSK, X: ndarray, y: ndarray, columns: list[str] | None = None, tune_outputs: bool = True, margin: float = 0.5, n_particles: int = 30, max_iter: int = 100, seed: int | None = None, **minimize_kwargs) -> PsoResult

Fit a system's membership-function parameters to data with PSO.

Extra keyword arguments are forwarded to :func:turboswarm.minimize (e.g. topology, patience, max_time).

Parameters:

Name Type Description Default
system Mamdani | TSK

a Mamdani or TSK system (mutated in place with the best params).

required
X ndarray

inputs, shape (n_samples, n_inputs).

required
y ndarray

targets, shape (n_samples,).

required
columns list[str] | None

input variable names matching X's columns, in order. Defaults to the variables the rules reference.

None
tune_outputs bool

also tune the output variables' MFs (Mamdani only).

True
margin float

search half-width per parameter as a fraction of its variable's universe span (0.5 searches ±50% of the span around each value).

0.5
n_particles int

swarm size.

30
max_iter int

PSO iterations.

100
seed int | None

PSO seed for reproducibility.

None

Returns:

Type Description
PsoResult

The turboswarm PsoResult (.best_position, .best_value, …).

Integrations — Optuna

fuzzytool.integrations.optuna

Optuna integration: search fuzzy-system structure and hyperparameters.

Install with pip install fuzzytool[optuna]. Fuzzy systems have plenty of discrete/continuous knobs — which t-norm, which defuzzifier, how many membership functions, what learning rate — that are awkward to grid-search by hand. These helpers turn an Optuna trial into a configured system, plus a ready-made :func:tune_anfis study.

  • :func:suggest_inference_spec — sample the connectives/implication/defuzz of a Mamdani system from a trial.
  • :func:suggest_anfis — build an :class:~fuzzytool.anfis.ANFIS with a trial-suggested n_mf and learning_rate.
  • :func:tune_anfis — run a full study that minimizes training RMSE and returns the best (refit) model.

suggest_inference_spec

suggest_inference_spec(trial, prefix: str = '') -> dict

Sample a Mamdani spec {tnorm, snorm, implication, defuzz} from a trial.

prefix namespaces the parameter names so several specs can coexist in one study. Pass the result straight to :class:~fuzzytool.inference.Mamdani.

suggest_anfis

suggest_anfis(trial, n_inputs: int, n_mf_range: tuple[int, int] = (2, 5), lr_range: tuple[float, float] = (0.001, 0.2)) -> ANFIS

Build an ANFIS with a trial-suggested n_mf and learning_rate.

tune_anfis

tune_anfis(X: ndarray, y: ndarray, n_trials: int = 20, n_mf_range: tuple[int, int] = (2, 5), lr_range: tuple[float, float] = (0.001, 0.2), epochs: int = 100, seed: int | None = None) -> tuple

Tune an ANFIS's n_mf/learning_rate with an Optuna study.

Parameters:

Name Type Description Default
X ndarray

inputs, shape (n_samples, n_inputs).

required
y ndarray

targets, shape (n_samples,).

required
n_trials int

number of Optuna trials.

20
n_mf_range tuple[int, int]

(min, max) membership functions per input to search.

(2, 5)
lr_range tuple[float, float]

(min, max) learning rate to search (log scale).

(0.001, 0.2)
epochs int

training epochs per trial.

100
seed int | None

seed for Optuna's sampler (reproducible search).

None

Returns:

Type Description
tuple

A (best_model, study) tuple: a fresh ANFIS trained with the best

tuple

hyperparameters, and the Optuna study.

Integrations — Joblib / Dask

fuzzytool.integrations.parallel

Parallel execution helpers (Joblib and Dask).

Install with pip install fuzzytool[parallel] (Joblib) or fuzzytool[dask] (Dask). These spread embarrassingly-parallel fuzzy workloads across cores or a Dask cluster:

  • :func:parallel_predict — chunked batch inference with Joblib.
  • :func:multi_start_cmeans — run fuzzy c-means from many seeds in parallel and keep the best (FCM is sensitive to initialization).
  • :func:dask_predict — the same chunked inference on a Dask scheduler.

.. note:: The process backends pickle the system. A hand-written Mamdani/TSK built from the built-in shapes pickles fine; a TSK with lambda consequents does not — pass backend="threading" for those.

parallel_predict

parallel_predict(system: Mamdani | TSK, X: ndarray, columns: list[str] | None = None, n_jobs: int = -1, n_chunks: int | None = None, backend: str = 'loky') -> np.ndarray | dict

Run batch inference over X in parallel chunks with Joblib.

Parameters:

Name Type Description Default
system Mamdani | TSK

a Mamdani or TSK system.

required
X ndarray

inputs, shape (n_samples, n_inputs).

required
columns list[str] | None

input variable names matching X's columns, in order. Defaults to the variables the rules reference.

None
n_jobs int

Joblib worker count (-1 = all cores).

-1
n_chunks int | None

number of row chunks (defaults to 4 * n_jobs or 8).

None
backend str

Joblib backend ("loky" processes, "threading" threads).

'loky'

Returns:

Type Description
ndarray | dict

The stacked predictions — an array (single output) or a dict of arrays.

multi_start_cmeans

multi_start_cmeans(X: ndarray, c: int, n_starts: int = 8, n_jobs: int = -1, backend: str = 'loky', **kwargs) -> ClusterResult

Run :func:~fuzzytool.cluster.fuzzy_cmeans from many seeds; keep the best.

Fuzzy c-means converges to a local optimum that depends on initialization; running several seeds and keeping the lowest-objective result is a standard safeguard. The runs are independent, so they parallelize cleanly. Extra keyword arguments are forwarded to :func:~fuzzytool.cluster.fuzzy_cmeans.

Parameters:

Name Type Description Default
X ndarray

data, shape (n_samples, n_features).

required
c int

number of clusters.

required
n_starts int

number of random restarts (seeds 0 .. n_starts - 1).

8
n_jobs int

Joblib worker count (-1 = all cores).

-1
backend str

Joblib backend.

'loky'

Returns:

Type Description
ClusterResult

The best :class:~fuzzytool.cluster.ClusterResult (minimum objective).

dask_predict

dask_predict(system: Mamdani | TSK, X: ndarray, columns: list[str] | None = None, n_chunks: int | None = None) -> np.ndarray | dict

Run chunked batch inference on a Dask scheduler.

Mirrors :func:parallel_predict but builds a Dask graph (dask.delayed) and computes it, so it scales to a distributed cluster.

Parameters:

Name Type Description Default
system Mamdani | TSK

a Mamdani or TSK system.

required
X ndarray

inputs, shape (n_samples, n_inputs).

required
columns list[str] | None

input variable names matching X's columns.

None
n_chunks int | None

number of row chunks (default 8).

None

Returns:

Type Description
ndarray | dict

The stacked predictions — an array or a dict of arrays.

Integrations — LLM agents

fuzzytool.integrations.agents

LLM-agent integration: expose a fuzzy system as an explainable tool.

Install with pip install fuzzytool[agents]. A fuzzy inference system is an unusually good citizen for tool-using LLMs: it is deterministic, bounded, and — unlike a black-box model — it can say why it produced an answer by reporting which rules fired and how strongly.

  • :func:explain — run a system and return its crisp output plus the fired rules (no third-party dependency; useful on its own).
  • :func:inference_tool — wrap a system as a LangChain StructuredTool an agent can call.

explain

explain(system, **inputs) -> dict

Run system on inputs and explain the result.

Returns a dict with the crisp output (a float, or a dict for a multi-output Mamdani) and fired_rules: the rules whose firing strength is positive, each {"index", "rule", "firing", "share"}, sorted strongest-first. This is the payload an agent (or a human) needs to trust the answer.

Thin wrapper over :meth:~fuzzytool.inference._common.RuleBase.explain, which every engine provides; kept here so agent code has one obvious import.

inference_tool

inference_tool(system: Mamdani | TSK, columns: list[str] | None = None, name: str = 'fuzzy_inference', description: str | None = None) -> StructuredTool

Wrap a fuzzy system as a LangChain StructuredTool.

The tool takes one float argument per input variable and returns the crisp output together with the rules that fired — so the agent can both use and explain the system.

Parameters:

Name Type Description Default
system Mamdani | TSK

a Mamdani or TSK system.

required
columns list[str] | None

input variable names (the tool's arguments). Defaults to the variables the rules reference.

None
name str

tool name exposed to the agent.

'fuzzy_inference'
description str | None

tool description; a sensible default is generated.

None

Returns:

Type Description
StructuredTool

A langchain_core.tools.StructuredTool.

Datasets

fuzzytool.datasets

Ready-made example systems.

credit_risk

credit_risk() -> tuple[Mamdani, Variable, Variable, Variable]

A credit-risk-premium Mamdani system.

Given a borrower's credit score (300-850) and dti (debt-to-income ratio, 0-50%), recommend the premium (risk points, 0-12) a lender should add on top of its base interest rate. Returns (system, score, dti, premium) so callers can inspect the variables (e.g. for plotting).

sys, score, dti, premium = credit_risk() safe = sys(score=800, dti=10) # great score, low leverage risky = sys(score=520, dti=42) # poor score, high leverage safe < risky True

credit_risk_it2

credit_risk_it2() -> tuple[IT2Mamdani, Variable, Variable, Variable]

An interval type-2 version of :func:credit_risk.

The score and premium terms carry a footprint of uncertainty (uncertain Gaussian means), modeling vagueness in how a "good" score or a "low" premium is defined. Returns (system, score, dti, premium).

sys, score, dti, premium = credit_risk_it2() sys(score=800, dti=10) < sys(score=520, dti=42) True

make_blobs

make_blobs(centers=((0.0, 0.0), (6.0, 6.0), (0.0, 6.0)), n_per: int = 60, spread: float = 0.7, seed: int | None = 0) -> np.ndarray

Synthetic isotropic Gaussian blobs for clustering demos and tests.

Returns the stacked data X of shape (len(centers) * n_per, n_features).

X = make_blobs(seed=0) X.shape (180, 2)