Skip to content

bulanik.inference

Rule bases and the three type-1 inference systems. The package re-exports its public names, but they are defined in four submodules, which are rendered one after another below: rules (rule syntax and explanations), Mamdani, Sugeno and Tsukamoto.

inference

Fuzzy rules and inference systems (type-1).

  • Rules: write them as text (see parse_rule for the syntax) or build them from Term, And, Or and Rule; firing_strength evaluates an antecedent.
  • Systems: MamdaniFIS (fuzzy output sets, defuzzified), SugenoFIS (constant or Linear consequents) and TsukamotoFIS (monotonic consequents inverted at the firing strength). All three offer evaluate for the crisp outputs and explain for an Explanation.
  • Interval type-2 versions live in bulanik.type2; the grid-partition ANFIS in bulanik.anfis.

Rules

rules

Fuzzy rules: antecedent objects, the rule grammar and firing strength.

Everything here is independent of the inference style, so Mamdani, Sugeno, Tsukamoto and interval type-2 systems share it. A rule is written as text and parsed by parse_rule (the inference systems do this for you when you pass strings), or built from the objects Term, And, Or and Rule.

Rule syntax

A rule has an antecedent (the condition), one or more consequents and an optional weight:

IF <antecedent> THEN <var> IS <term> [AND <var> IS <term> ...] [WITH <weight>]
  • Keywords are IF, THEN, IS, NOT, AND, OR and WITH. They are case-insensitive (if, If and IF are the same) and ASCII only. A keyword cannot be used as a variable or term name.
  • Names (variables and terms) start with a letter or an underscore and continue with letters, digits or underscores; there are no spaces, so very_high and x2 are names but very high is two. Letters outside ASCII are allowed.
  • Atoms are variable IS term and variable IS NOT term. NOT negates the membership degree, \(1 - \mu\), and is only allowed directly after IS in an antecedent; it cannot appear in a consequent.
  • Operators: AND binds tighter than OR, and parentheses group, so a IS x OR b IS y AND c IS z means a IS x OR (b IS y AND c IS z). Chains such as p AND q AND r are n-ary and are combined from left to right.
  • Consequents follow THEN and are joined with AND, one per output variable (naming an output twice in one rule is an error). A rule may leave an output variable out; it then does not contribute to that output.
  • Weight: WITH w multiplies the rule's firing strength by a number \(w \in [0, 1]\) and defaults to 1. A weight outside \([0, 1]\) raises ValueError.

The firing strength of an antecedent is computed from the membership degrees of the crisp inputs. For atoms \(\mu\) is the degree (or \(1 - \mu\) if negated); AND is a t-norm (\(\min\) or the product, and_method of the system) and OR a t-conorm (\(\max\) or the probabilistic sum \(a + b - ab\), or_method); see t_norm and t_conorm. The activation of a rule is its weight times that strength.

Parsing and printing round-trip: str(rule) is canonical upper-case rule text that parse_rule reads back to an equal rule.

Examples:

>>> from bulanik.inference import parse_rule
>>> rule = parse_rule("if food is not rancid and (service is good or service is ok) then tip is high with 0.8")
>>> print(rule)
IF food IS NOT rancid AND (service IS good OR service IS ok) THEN tip IS high WITH 0.8
>>> rule.weight, dict(rule.consequents)
(0.8, {'tip': 'high'})
>>> parse_rule(str(rule)) == rule
True
>>> parse_rule("IF a IS b THEN c IS NOT d")
Traceback (most recent call last):
...
ValueError: NOT is not allowed in a consequent (offset 20) in rule 'IF a IS b THEN c IS NOT d'

Antecedent module-attribute

Antecedent = Union[Term, And, Or]

An antecedent expression: a Term, or an And / Or of antecedents.

NoRuleFiredError

NoRuleFiredError(message: str, *, output: str | None = None, explanation: Explanation | None = None)

Bases: ValueError

No rule fired for an output and no default value was given.

A ValueError raised by evaluate / explain of the inference systems. Give the system a default to return a fixed value instead.

Parameters:

Name Type Description Default
message str

The error message.

required
output str | None

Name of the output variable no rule fired for.

None
explanation Explanation | None

The partial Explanation of the failed evaluation.

None

Attributes:

Name Type Description
output

The output variable name, or None when the error is raised without that context.

explanation

The input degrees, every rule's activations and the outputs that did succeed before this one; None without context.

Examples:

>>> from bulanik.sets import LinguisticVariable, triangular
>>> from bulanik.inference import MamdaniFIS, NoRuleFiredError
>>> x = LinguisticVariable("x", (0, 10), {"lo": triangular(0, 0, 3)})
>>> y = LinguisticVariable("y", (0, 10), {"low": triangular(0, 0, 10)})
>>> fis = MamdaniFIS([x], [y], ["IF x IS lo THEN y IS low"])
>>> try:
...     fis.evaluate({"x": 8})
... except NoRuleFiredError as err:
...     print(err.output, err.explanation.activations[0].strength)
y 0.0

Term dataclass

Term(variable: str, term: str, negated: bool = False)

The atom variable IS term, or variable IS NOT term when negated.

The operators &, | and ~ build antecedents: a & b is And, a | b is Or and ~a is the negated term.

Parameters:

Name Type Description Default
variable str

Name of an input variable.

required
term str

Name of one of its terms.

required
negated bool

Whether the membership degree is negated, \(1 - \mu\).

False

Raises:

Type Description
TypeError

If a name is not a string, or negated is not a bool.

ValueError

If a name is not a valid rule name or is a rule keyword (see the module description for the syntax).

Attributes:

Name Type Description
variable str

The variable name.

term str

The term name.

negated bool

Whether the term is negated.

Examples:

>>> from bulanik.inference import Term
>>> food, service = Term("food", "good"), Term("service", "poor")
>>> print(food & ~service)
food IS good AND service IS NOT poor
>>> print((food | service) & Term("price", "low"))
(food IS good OR service IS poor) AND price IS low

And dataclass

And(parts: tuple[Antecedent, ...])

Conjunction of antecedents (AND); nested conjunctions are flattened.

Evaluated with the system's t-norm, folded from left to right.

Parameters:

Name Type Description Default
parts tuple[Antecedent, ...]

Two or more antecedents.

required

Raises:

Type Description
ValueError

If fewer than two parts remain after flattening.

Attributes:

Name Type Description
parts tuple[Antecedent, ...]

The flattened parts.

Examples:

>>> from bulanik.inference import And, Term
>>> a, b, c = Term("x", "a"), Term("x", "b"), Term("y", "c")
>>> And((And((a, b)), c)).parts == (a, b, c)
True
>>> print(a & b & c)
x IS a AND x IS b AND y IS c

Or dataclass

Or(parts: tuple[Antecedent, ...])

Disjunction of antecedents (OR); nested disjunctions are flattened.

Evaluated with the system's t-conorm, folded from left to right.

Parameters:

Name Type Description Default
parts tuple[Antecedent, ...]

Two or more antecedents.

required

Raises:

Type Description
ValueError

If fewer than two parts remain after flattening.

Attributes:

Name Type Description
parts tuple[Antecedent, ...]

The flattened parts.

Examples:

>>> from bulanik.inference import Or, Term
>>> a, b = Term("x", "a"), Term("y", "b")
>>> print(Or((a, b)))
x IS a OR y IS b
>>> Or((a,))
Traceback (most recent call last):
...
ValueError: Or needs at least two parts

Rule dataclass

Rule(antecedent: Antecedent, consequents: Mapping[str, str], weight: float = 1.0)

IF antecedent THEN consequents [WITH weight].

A rule is immutable and hashable; str(rule) is canonical rule text that parse_rule reads back. Rules given to an inference system are checked against its variables and terms.

Parameters:

Name Type Description Default
antecedent Antecedent

A Term, And or Or.

required
consequents Mapping[str, str]

Mapping from output variable name to term name (at least one).

required
weight float

Rule weight in \([0, 1]\); it multiplies the firing strength.

1.0

Raises:

Type Description
TypeError

If antecedent has the wrong type, or a name is not a string.

ValueError

If there are no consequents, a name is invalid or a rule keyword, or weight is not finite or outside \([0, 1]\).

Attributes:

Name Type Description
antecedent Antecedent

The condition.

consequents Mapping[str, str]

A read-only mapping of output variable to term; mutating it raises TypeError.

weight float

The rule weight as a float.

Examples:

>>> from bulanik.inference import Rule, Term
>>> rule = Rule(Term("service", "good") | Term("food", "good"), {"tip": "high"}, 0.5)
>>> print(rule)
IF service IS good OR food IS good THEN tip IS high WITH 0.5
>>> Rule(Term("a", "b"), {"c": "d"}, 1.5)
Traceback (most recent call last):
...
ValueError: rule weight 1.5 must be in [0, 1]

RuleActivation dataclass

RuleActivation(rule: str, strength: float | tuple[float, float])

A rule's firing level: weight times antecedent degree.

Interval type-2 systems store a (lower, upper) pair instead of a float.

Parameters:

Name Type Description Default
rule str

The rule text, as str(rule).

required
strength float | tuple[float, float]

The activation, a float in \([0, 1]\) (or a pair for interval type-2 systems).

required

Attributes:

Name Type Description
rule str

The rule text.

strength float | tuple[float, float]

The activation.

Examples:

>>> from bulanik.inference import RuleActivation
>>> RuleActivation("IF x IS a THEN y IS b", 0.25).strength
0.25

Explanation dataclass

Explanation(degrees: dict[str, dict[str, float | tuple[float, float]]], activations: tuple[RuleActivation, ...], outputs: dict[str, float] = dict())

Everything behind one evaluation: degrees, rule activations and outputs.

Returned by the explain method of every inference system; its outputs are what evaluate returns.

Parameters:

Name Type Description Default
degrees dict[str, dict[str, float | tuple[float, float]]]

Membership degree of each input in each of its terms, as degrees[variable][term] (a (lower, upper) pair for interval type-2 systems).

required
activations tuple[RuleActivation, ...]

One RuleActivation per rule, in rule order.

required
outputs dict[str, float]

The crisp value of each output variable.

dict()

Attributes:

Name Type Description
degrees dict[str, dict[str, float | tuple[float, float]]]

The input membership degrees.

activations tuple[RuleActivation, ...]

The rule activations.

outputs dict[str, float]

The crisp outputs.

Examples:

>>> from bulanik.sets import LinguisticVariable, triangular
>>> from bulanik.inference import MamdaniFIS
>>> x = LinguisticVariable("x", (0, 10), {"lo": triangular(0, 0, 10), "hi": triangular(0, 10, 10)})
>>> y = LinguisticVariable("y", (0, 10), {"lo": triangular(0, 0, 10), "hi": triangular(0, 10, 10)})
>>> fis = MamdaniFIS([x], [y], ["IF x IS lo THEN y IS lo", "IF x IS hi THEN y IS hi WITH 0.5"])
>>> info = fis.explain({"x": 2.5})
>>> info.degrees
{'x': {'lo': 0.75, 'hi': 0.25}}
>>> [a.strength for a in info.activations]
[0.75, 0.125]
>>> sorted(info.outputs)
['y']

rule_terms

rule_terms(antecedent: Antecedent) -> list[Term]

All Term leaves of an antecedent, left to right.

Parameters:

Name Type Description Default
antecedent Antecedent

A Term, And or Or.

required

Returns:

Type Description
list[Term]

The terms in the order they appear in the rule text.

Examples:

>>> from bulanik.inference import Term
>>> from bulanik.inference.rules import rule_terms
>>> [str(t) for t in rule_terms((Term("a", "x") | Term("b", "y")) & ~Term("c", "z"))]
['a IS x', 'b IS y', 'c IS NOT z']

firing_strength

firing_strength(antecedent: Antecedent, degrees: Mapping[str, Mapping[str, float]], and_method: str = 'min', or_method: str = 'max') -> float

Degree to which an antecedent holds.

A term has its membership degree \(\mu\), or \(1 - \mu\) when negated. And folds the parts with the t-norm and_method and Or with the t-conorm or_method, from left to right (see t_norm and t_conorm). The rule weight is not applied here.

Parameters:

Name Type Description Default
antecedent Antecedent

A Term, And or Or.

required
degrees Mapping[str, Mapping[str, float]]

Membership degrees as degrees[variable][term].

required
and_method str

"min" or "prod".

'min'
or_method str

"max" or "probor".

'max'

Returns:

Type Description
float

The firing strength in \([0, 1]\).

Raises:

Type Description
ValueError

If a method name is unknown, or a term of the antecedent is missing from degrees.

Examples:

>>> from bulanik.inference import firing_strength, parse_rule
>>> rule = parse_rule("IF x IS a AND y IS b THEN z IS c")
>>> degrees = {"x": {"a": 0.6}, "y": {"b": 0.5}}
>>> firing_strength(rule.antecedent, degrees)
0.5
>>> firing_strength(rule.antecedent, degrees, and_method="prod")
0.3

parse_rule

parse_rule(text: str) -> Rule

Parse rule text IF ... THEN ... [WITH w] into a Rule.

The grammar is described in the module documentation: keywords are case-insensitive, AND binds tighter than OR, parentheses group, and NOT only follows IS.

Parameters:

Name Type Description Default
text str

The rule text.

required

Returns:

Type Description
Rule

The parsed rule.

Raises:

Type Description
TypeError

If text is not a string.

ValueError

If the text is not a valid rule (the message gives the offset), a keyword is used as a name, a consequent is negated or repeated, or the weight is outside \([0, 1]\).

Examples:

>>> from bulanik.inference import parse_rule
>>> rule = parse_rule("IF temp IS hot OR NOT humidity IS low THEN fan IS fast")
Traceback (most recent call last):
...
ValueError: keyword 'NOT' used as a name at offset 18 in rule 'IF temp IS hot OR NOT humidity IS low THEN fan IS fast'
>>> rule = parse_rule("IF temp IS hot OR humidity IS NOT low THEN fan IS fast WITH 0.7")
>>> rule.weight, str(rule.antecedent)
(0.7, 'temp IS hot OR humidity IS NOT low')

Mamdani

mamdani

Mamdani fuzzy inference over sampled output universes.

MamdaniFIS fuzzifies crisp inputs, fires the rules (see parse_rule for the rule syntax), clips or scales each consequent set by its rule's firing strength, aggregates the clipped sets over a sampled output universe and defuzzifies the result with defuzzify_samples.

MamdaniFIS

MamdaniFIS(inputs: Sequence[LinguisticVariable], outputs: Sequence[LinguisticVariable], rules: Sequence[Rule | str], *, and_method: str = 'min', or_method: str = 'max', implication: str = 'min', aggregation: str = 'max', defuzzification: str = 'centroid', resolution: int = 1001, default: Mapping[str, float] | None = None)

Mamdani fuzzy inference system (Mamdani and Assilian 1975).

For crisp inputs the system computes each rule's firing strength \(\alpha_k\) (rule weight times the antecedent degree, with the t-norm and_method and t-conorm or_method). Every output universe is sampled at resolution evenly spaced points \(x_i\), endpoints included, and the aggregated set is

\[\mu(x_i) = \operatorname{agg}_k \; \operatorname{imp}\bigl(\alpha_k,\; \mu_{B_k}(x_i)\bigr)\]

where \(B_k\) is the consequent term of rule \(k\). implication is "min" (clip, \(\min(\alpha, \mu)\)) or "prod" (scale, \(\alpha\mu\)); aggregation is "max", "sum" (not clipped to 1, as in MATLAB) or "probor" (\(a + b - ab\)). The crisp output is the defuzzification of the aggregated samples, see defuzzify_samples. If no rule fires for an output (all aggregated samples are 0) the value from default is returned, or NoRuleFiredError is raised.

Parameters:

Name Type Description Default
inputs Sequence[LinguisticVariable] required
outputs Sequence[LinguisticVariable]

Output linguistic variables (at least one; names distinct from the inputs).

required
rules Sequence[Rule | str]

Rule objects and/or rule text.

required
and_method str

"min" or "prod".

'min'
or_method str

"max" or "probor".

'max'
implication str

"min" or "prod".

'min'
aggregation str

"max", "sum" or "probor".

'max'
defuzzification str

"centroid", "bisector", "mom", "som" or "lom".

'centroid'
resolution int

Samples per output universe, an integer of at least 2.

1001
default Mapping[str, float] | None

Output value per output variable, used when no rule fires (inside the universe). None raises instead.

None

Raises:

Type Description
TypeError

If a variable is not a LinguisticVariable or a rule is neither a Rule nor a string.

ValueError

If an option name is unknown, resolution is not an integer of at least 2, there are no inputs, outputs or rules, a variable name is duplicated, a rule names an unknown variable or term, an output term used by a rule has membership 0 at every sample, or default is not exactly one in-universe value per output.

Attributes:

Name Type Description
inputs

The input variables (read-only).

outputs

The output variables (read-only).

rules

The parsed rules (read-only).

and_method

The t-norm name.

or_method

The t-conorm name.

implication

The implication name.

aggregation

The aggregation name.

defuzzification

The defuzzification name.

resolution

Samples per output universe.

default

A copy of the no-fire defaults, or None.

Examples:

>>> from bulanik.sets import LinguisticVariable, triangular
>>> from bulanik.inference import MamdaniFIS
>>> service = LinguisticVariable("service", (0, 10), {
...     "poor": triangular(0, 0, 5), "good": triangular(5, 10, 10)})
>>> tip = LinguisticVariable("tip", (0, 20), {
...     "low": triangular(0, 0, 10), "high": triangular(10, 20, 20)})
>>> fis = MamdaniFIS([service], [tip], [
...     "IF service IS poor THEN tip IS low",
...     "IF service IS good THEN tip IS high"])
>>> fis.evaluate({"service": 7.5})
{'tip': 16.116...}
>>> fis.evaluate({"service": 2})
{'tip': 3.708...}
References

Mamdani and Assilian (1975): "An experiment in linguistic synthesis with a fuzzy logic controller", International Journal of Man-Machine Studies.

explain

explain(inputs: Mapping[str, float]) -> Explanation

Evaluate and report degrees, rule activations and crisp outputs.

Parameters:

Name Type Description Default
inputs Mapping[str, float]

Crisp value of every input variable, by name. The keys must be exactly the input names and each value must lie inside its variable's universe (values are never clamped).

required

Returns:

Type Description
Explanation

An Explanation with the

Explanation

input membership degrees, one activation per rule and the crisp

Explanation

outputs.

Raises:

Type Description
TypeError

If a value is a bool or a string.

ValueError

If an input is missing, unexpected, not finite or outside its universe.

NoRuleFiredError

If no rule fires for an output and there is no default; its explanation holds the partial result.

Examples:

>>> from bulanik.sets import LinguisticVariable, triangular
>>> from bulanik.inference import MamdaniFIS
>>> x = LinguisticVariable("x", (0, 10), {"lo": triangular(0, 0, 10), "hi": triangular(0, 10, 10)})
>>> y = LinguisticVariable("y", (0, 10), {"lo": triangular(0, 0, 10), "hi": triangular(0, 10, 10)})
>>> fis = MamdaniFIS([x], [y], ["IF x IS lo THEN y IS lo", "IF x IS hi THEN y IS hi"])
>>> info = fis.explain({"x": 2.5})
>>> info.degrees
{'x': {'lo': 0.75, 'hi': 0.25}}
>>> info.activations[0]
RuleActivation(rule='IF x IS lo THEN y IS lo', strength=0.75)
>>> fis.explain({"x": 11})
Traceback (most recent call last):
...
ValueError: 'x' input 11 is outside its universe [0, 10]

evaluate

evaluate(inputs: Mapping[str, float]) -> dict[str, float]

Crisp outputs for crisp inputs.

Parameters:

Name Type Description Default
inputs Mapping[str, float]

Crisp value of every input variable, by name.

required

Returns:

Type Description
dict[str, float]

A dict from output variable name to its crisp value.

Raises:

Type Description
TypeError

If a value is a bool or a string.

ValueError

If an input is missing, unexpected, not finite or outside its universe.

NoRuleFiredError

If no rule fires for an output and there is no default.

Examples:

>>> from bulanik.sets import LinguisticVariable, triangular
>>> from bulanik.inference import MamdaniFIS
>>> x = LinguisticVariable("x", (0, 10), {"lo": triangular(0, 0, 3), "hi": triangular(7, 10, 10)})
>>> y = LinguisticVariable("y", (0, 20), {"low": triangular(0, 0, 10), "high": triangular(10, 20, 20)})
>>> rules = ["IF x IS lo THEN y IS low", "IF x IS hi THEN y IS high"]
>>> MamdaniFIS([x], [y], rules, default={"y": 10}).evaluate({"x": 5})
{'y': 10.0}
>>> MamdaniFIS([x], [y], rules).evaluate({"x": 5})
Traceback (most recent call last):
...
bulanik.inference.rules.NoRuleFiredError: no rule fired for output 'y' and no default was given; the error's .explanation holds the input degrees and rule activations

defuzzify_samples

defuzzify_samples(xs: Sequence[float], mus: Sequence[float], method: str) -> float

Crisp value of a sampled fuzzy set.

With samples \((x_i, \mu_i)\) the methods are:

  • "centroid": \(\sum_i x_i \mu_i / \sum_i \mu_i\).
  • "bisector": the first \(x_i\) where the running sum of \(\mu\) reaches half of the total.
  • "mom", "som", "lom": the mean, smallest and largest \(x_i\) among the samples whose \(\mu_i\) is within \(10^{-12}\) of the maximum.

Memberships may exceed 1 ("sum" aggregation). Tiny negative values within the library tolerance are treated as 0.

Parameters:

Name Type Description Default
xs Sequence[float]

Sample points (finite).

required
mus Sequence[float]

Membership at each sample, same length as xs.

required
method str

"centroid", "bisector", "mom", "som" or "lom".

required

Returns:

Type Description
float

The crisp value.

Raises:

Type Description
ValueError

If method is unknown, the lengths differ, a value is not finite, a membership is negative, or every membership is zero (the set has no crisp value).

Examples:

>>> from bulanik.inference import defuzzify_samples
>>> xs, mus = [0, 1, 2, 3], [0, 1, 1, 0]
>>> [defuzzify_samples(xs, mus, m) for m in ("centroid", "bisector", "mom", "som", "lom")]
[1.5, 1, 1.5, 1, 2]
>>> defuzzify_samples(xs, [0, 0, 0, 0], "centroid")
Traceback (most recent call last):
...
ValueError: cannot defuzzify a set whose memberships are all zero

Sugeno

sugeno

Takagi-Sugeno-Kang fuzzy inference: constant or linear rule consequents.

A Sugeno system has no output membership functions. Each output variable (SugenoOutput) maps its term names to a constant (zero-order) or a Linear function of the inputs (first-order). SugenoFIS combines the fired rules' values by a weighted average.

Linear

Linear(coefficients: Mapping[str, float], constant: float = 0.0)

First-order Sugeno consequent \(c_0 + \sum_i c_i x_i\) over named inputs.

Calling the object evaluates it on a mapping of crisp inputs; extra inputs are ignored.

Parameters:

Name Type Description Default
coefficients Mapping[str, float]

Mapping from input variable name to its coefficient \(c_i\).

required
constant float

The constant term \(c_0\).

0.0

Raises:

Type Description
TypeError

If a coefficient or constant is a bool or a string.

ValueError

If a coefficient or constant is not finite.

Attributes:

Name Type Description
coefficients

A copy of the input name to coefficient mapping.

constant

The constant term.

Examples:

>>> from bulanik.inference import Linear
>>> f = Linear({"x": 2.0, "y": -1.0}, 0.5)
>>> f({"x": 3, "y": 1})
5.5
>>> f
Linear({'x': 2.0, 'y': -1.0}, 0.5)
>>> f({"x": 3})
Traceback (most recent call last):
...
ValueError: missing input 'y' for Linear

SugenoOutput

SugenoOutput(name: str, functions: Mapping[str, float | Linear])

Output variable whose terms are constants (zero-order) or Linear (first-order).

Parameters:

Name Type Description Default
name str

The output name, used in rule consequents.

required
functions Mapping[str, float | Linear]

Mapping from term name to a number or a Linear (at least one term).

required

Raises:

Type Description
ValueError

If name is not a non-empty string, functions is empty, or a constant is not finite.

TypeError

If a term name is not a string, or a constant is a bool or a string.

Attributes:

Name Type Description
name

The output name.

functions

A copy of the term to constant-or-Linear mapping.

term_names tuple[str, ...]

The term names, in insertion order.

Examples:

>>> from bulanik.inference import Linear, SugenoOutput
>>> speed = SugenoOutput("speed", {"slow": 10, "fast": Linear({"load": -2.0}, 80)})
>>> speed.term_names
('slow', 'fast')
>>> speed
SugenoOutput('speed', {'slow': 10.0, 'fast': Linear({'load': -2.0}, 80.0)})

term_names property

term_names: tuple[str, ...]

The term names, in insertion order.

SugenoFIS

SugenoFIS(inputs: Sequence[LinguisticVariable], outputs: Sequence[SugenoOutput], rules: Sequence[Rule | str], *, and_method: str = 'min', or_method: str = 'max', output_method: str = 'weighted_average', default: Mapping[str, float] | None = None)

Sugeno fuzzy inference system (Takagi and Sugeno 1985).

Each fired rule \(k\) has firing strength \(\alpha_k\) (rule weight times the antecedent degree) and a crisp value \(z_k\): the consequent constant, or the Linear function evaluated at the crisp inputs. The output is

\[y = \frac{\sum_k \alpha_k z_k}{\sum_k \alpha_k}\]

for output_method="weighted_average" and \(y = \sum_k \alpha_k z_k\) for "weighted_sum".

When no rule fires for an output (\(\sum_k \alpha_k = 0\)) the value from default is returned, or NoRuleFiredError is raised.

Parameters:

Name Type Description Default
inputs Sequence[LinguisticVariable] required
outputs Sequence[SugenoOutput]

SugenoOutputs (at least one; names distinct from the inputs).

required
rules Sequence[Rule | str]

Rule objects and/or rule text.

required
and_method str

"min" or "prod".

'min'
or_method str

"max" or "probor".

'max'
output_method str

"weighted_average" or "weighted_sum".

'weighted_average'
default Mapping[str, float] | None

Output value per output variable, used when no rule fires.

None

Raises:

Type Description
TypeError

If a variable has the wrong class or a rule is neither a Rule nor a string.

ValueError

If an option name is unknown, there are no inputs, outputs or rules, a variable name is duplicated, a rule names an unknown variable or term, a Linear names an unknown input, or default is not exactly one finite value per output.

Attributes:

Name Type Description
inputs

The input variables (read-only).

outputs

The output variables (read-only).

rules

The parsed rules (read-only).

and_method

The t-norm name.

or_method

The t-conorm name.

output_method

The output combination name.

default

A copy of the no-fire defaults, or None.

Examples:

>>> from bulanik.sets import LinguisticVariable, linear_decreasing, linear_increasing
>>> from bulanik.inference import Linear, SugenoFIS, SugenoOutput
>>> x = LinguisticVariable("x", (0, 10), {
...     "lo": linear_decreasing(0, 10), "hi": linear_increasing(0, 10)})
>>> y = SugenoOutput("y", {"a": 1.0, "b": Linear({"x": 2.0}, 1.0)})
>>> rules = ["IF x IS lo THEN y IS a", "IF x IS hi THEN y IS b"]
>>> SugenoFIS([x], [y], rules).evaluate({"x": 2.5})
{'y': 2.25}
>>> weighted = ["IF x IS lo THEN y IS a WITH 0.5", "IF x IS hi THEN y IS b"]
>>> SugenoFIS([x], [y], weighted).evaluate({"x": 2.5})
{'y': 3.0}
>>> SugenoFIS([x], [y], weighted, output_method="weighted_sum").evaluate({"x": 2.5})
{'y': 1.875}
References

Takagi and Sugeno (1985): "Fuzzy identification of systems and its applications to modeling and control", IEEE Transactions on Systems, Man, and Cybernetics.

explain

explain(inputs: Mapping[str, float]) -> Explanation

Evaluate and report degrees, rule activations and crisp outputs.

Parameters:

Name Type Description Default
inputs Mapping[str, float]

Crisp value of every input variable, by name. The keys must be exactly the input names and each value must lie inside its variable's universe.

required

Returns:

Type Description
Explanation

An Explanation with the

Explanation

input membership degrees, one activation per rule and the crisp

Explanation

outputs.

Raises:

Type Description
TypeError

If a value is a bool or a string.

ValueError

If an input is missing, unexpected, not finite or outside its universe.

NoRuleFiredError

If no rule fires for an output and there is no default.

Examples:

>>> from bulanik.sets import LinguisticVariable, linear_decreasing, linear_increasing
>>> from bulanik.inference import SugenoFIS, SugenoOutput
>>> x = LinguisticVariable("x", (0, 10), {
...     "lo": linear_decreasing(0, 10), "hi": linear_increasing(0, 10)})
>>> y = SugenoOutput("y", {"a": 0.0, "b": 100.0})
>>> fis = SugenoFIS([x], [y], ["IF x IS lo THEN y IS a", "IF x IS hi THEN y IS b"])
>>> info = fis.explain({"x": 2.5})
>>> [a.strength for a in info.activations], info.outputs
([0.75, 0.25], {'y': 25.0})

evaluate

evaluate(inputs: Mapping[str, float]) -> dict[str, float]

Crisp outputs for crisp inputs.

Parameters:

Name Type Description Default
inputs Mapping[str, float]

Crisp value of every input variable, by name.

required

Returns:

Type Description
dict[str, float]

A dict from output variable name to its crisp value.

Raises:

Type Description
TypeError

If a value is a bool or a string.

ValueError

If an input is missing, unexpected, not finite or outside its universe.

NoRuleFiredError

If no rule fires for an output and there is no default.

Examples:

>>> from bulanik.sets import LinguisticVariable, triangular
>>> from bulanik.inference import SugenoFIS, SugenoOutput
>>> x = LinguisticVariable("x", (0, 10), {"lo": triangular(0, 0, 3)})
>>> y = SugenoOutput("y", {"a": 5})
>>> SugenoFIS([x], [y], ["IF x IS lo THEN y IS a"], default={"y": -1}).evaluate({"x": 8})
{'y': -1.0}

Tsukamoto

tsukamoto

Tsukamoto fuzzy inference: monotonic consequents inverted at the firing strength.

TsukamotoFIS needs output terms with a monotonic membership function; each fired rule reads its crisp value off the term at the rule's firing strength, and the output is the weighted average of those values.

TsukamotoFIS

TsukamotoFIS(inputs: Sequence[LinguisticVariable], outputs: Sequence[LinguisticVariable], rules: Sequence[Rule | str], *, and_method: str = 'min', or_method: str = 'max', default: Mapping[str, float] | None = None)

Tsukamoto fuzzy inference system (Tsukamoto 1979).

Every output term must be monotonic (linear_increasing, linear_decreasing or sigmoid). A fired rule \(k\) with firing strength \(\alpha_k\) (rule weight times the antecedent degree) gives the crisp value \(z_k = \mu_{B_k}^{-1}(\alpha_k)\), the point where its consequent term reaches \(\alpha_k\), and the output is

\[y = \frac{\sum_k \alpha_k z_k}{\sum_k \alpha_k}\]

over the rules with \(\alpha_k > 0\). Each \(z_k\) is clamped into the output's universe (the inverse of a sigmoid or of a linear ramp can fall outside it), and so is the final average (rounding can put it one ulp outside); inputs are never clamped. If no rule fires for an output the value from default is returned, or NoRuleFiredError is raised.

Parameters:

Name Type Description Default
inputs Sequence[LinguisticVariable] required
outputs Sequence[LinguisticVariable]

Output linguistic variables whose terms are all monotonic.

required
rules Sequence[Rule | str]

Rule objects and/or rule text.

required
and_method str

"min" or "prod".

'min'
or_method str

"max" or "probor".

'max'
default Mapping[str, float] | None

Output value per output variable, used when no rule fires.

None

Raises:

Type Description
TypeError

If a variable is not a LinguisticVariable or a rule is neither a Rule nor a string.

ValueError

If an option name is unknown, an output term is not monotonic, there are no inputs, outputs or rules, a variable name is duplicated, a rule names an unknown variable or term, or default is not exactly one in-universe value per output.

Attributes:

Name Type Description
inputs

The input variables (read-only).

outputs

The output variables (read-only).

rules

The parsed rules (read-only).

and_method

The t-norm name.

or_method

The t-conorm name.

default

A copy of the no-fire defaults, or None.

Examples:

>>> from bulanik.sets import LinguisticVariable, linear_decreasing, linear_increasing
>>> from bulanik.inference import TsukamotoFIS
>>> x = LinguisticVariable("x", (0, 10), {
...     "lo": linear_decreasing(0, 10), "hi": linear_increasing(0, 10)})
>>> z = LinguisticVariable("z", (0, 20), {
...     "low": linear_decreasing(0, 10), "high": linear_increasing(0, 20)})
>>> fis = TsukamotoFIS([x], [z], ["IF x IS lo THEN z IS low", "IF x IS hi THEN z IS high"])
>>> fis.evaluate({"x": 2.5})
{'z': 3.125}
References

Tsukamoto (1979): "An approach to fuzzy reasoning method", in Advances in Fuzzy Set Theory and Applications (North-Holland, Amsterdam).

explain

explain(inputs: Mapping[str, float]) -> Explanation

Evaluate and report degrees, rule activations and crisp outputs.

Parameters:

Name Type Description Default
inputs Mapping[str, float]

Crisp value of every input variable, by name. The keys must be exactly the input names and each value must lie inside its variable's universe.

required

Returns:

Type Description
Explanation

An Explanation with the

Explanation

input membership degrees, one activation per rule and the crisp

Explanation

outputs.

Raises:

Type Description
TypeError

If a value is a bool or a string.

ValueError

If an input is missing, unexpected, not finite or outside its universe.

NoRuleFiredError

If no rule fires for an output and there is no default.

Examples:

>>> from bulanik.sets import LinguisticVariable, linear_decreasing, linear_increasing
>>> from bulanik.inference import TsukamotoFIS
>>> x = LinguisticVariable("x", (0, 10), {"hi": linear_increasing(0, 10)})
>>> z = LinguisticVariable("z", (0, 20), {"high": linear_increasing(0, 20)})
>>> info = TsukamotoFIS([x], [z], ["IF x IS hi THEN z IS high"]).explain({"x": 5})
>>> info.activations[0].strength, info.outputs
(0.5, {'z': 10.0})

evaluate

evaluate(inputs: Mapping[str, float]) -> dict[str, float]

Crisp outputs for crisp inputs.

Parameters:

Name Type Description Default
inputs Mapping[str, float]

Crisp value of every input variable, by name.

required

Returns:

Type Description
dict[str, float]

A dict from output variable name to its crisp value.

Raises:

Type Description
TypeError

If a value is a bool or a string.

ValueError

If an input is missing, unexpected, not finite or outside its universe.

NoRuleFiredError

If no rule fires for an output and there is no default.

Examples:

>>> from bulanik.sets import LinguisticVariable, linear_increasing
>>> from bulanik.inference import TsukamotoFIS
>>> x = LinguisticVariable("x", (0, 10), {"hi": linear_increasing(0, 10)})
>>> z = LinguisticVariable("z", (0, 20), {"high": linear_increasing(0, 20)})
>>> TsukamotoFIS([x], [z], ["IF x IS hi THEN z IS high"]).evaluate({"x": 10})
{'z': 20.0}