Source code for fuzzy_expert.inference

"""
Inference Method
===============================================================================

"""
from __future__ import annotations

from typing import List, Union

import matplotlib.pyplot as plt
import numpy as np
from ipywidgets import interact, widgets

from fuzzy_expert.operators import (
    bounded_prod,
    bounded_sum,
    defuzzificate,
    drastic_prod,
    drastic_sum,
    maximum,
    minimum,
    prob_or,
    product,
)
from fuzzy_expert.plots import plot_crisp_input, plot_fuzzy_input
from fuzzy_expert.variable import FuzzyVariable

# from fuzzy_expert.operators import get_modified_membership, probor, defuzzificate
#


[docs]class DecompositionalInference: """ Decompositional inference method. :param and_operator: AND operator method for combining the compositions of propositions in a fuzzy rule premise, specified as one of the following: * `"min"`. * `"prod"`. * `"bunded_prod"`. * `"drastic_prod"`. :param or_operator: OR operator method for combining the compositions of propositions in a fuzzy rule premise, specified as one of the following: * `"max"`. * `"prob_or"`. * `"bounded_sum"`. * `"drastic_sum"`. :param implication_operator: method for computing the compositions of propositions in a fuzzy rule premise, specified as one of the following: * `"Ra"`. * `"Rm"`. * `"Rc"`. * `"Rb"`. * `"Rs"`. * `"Rg"`. * `"Rsg"`. * `"Rgs"`. * `"Rgg"`. * `"Rss"`. :param production_link: method for aggregating the consequences of the fuzzy rules, specified as one of the following: * `"min"`. * `"prod"`. * `"bunded_prod"`. * `"drastic_prod"`. * `"max"`. * `"prob_or"`. * `"bounded_sum"`. * `"drastic_sum"`. :param defuzzification_operator: Method for defuzzificate the resulting membership function, specified as one of the following: * `"cog"`: Center of gravity. * `"boa"`: Bisector of area. * `"mom"`: Mean of the values for which the membership function is maximum. * `"lom"`: Largest value for which the membership function is maximum. * `"som"`: Smallest value for which the membership function is minimum. """ def __init__( self, and_operator, or_operator, implication_operator, composition_operator, production_link, defuzzification_operator, ): self.and_operator = and_operator self.or_operator = or_operator self.composition_operator = composition_operator self.production_link = production_link self.defuzzification_operator = defuzzification_operator self.implication_operator = implication_operator def __call__(self, variables, rules, **input_values): # # Components of a fis. # self.variables = variables self.rules = rules self.input_values: dict = input_values self._convert_inputs_to_facts() self._fuzzificate_facts() self._compute_modified_premise_memberships() self._compute_modified_consequence_memberships() self._compute_fuzzy_implication() self._compute_fuzzy_composition() self._combine_antecedents() self._compute_rule_infered_cf() self._collect_rule_memberships() self._aggregate_collected_memberships() self._aggregate_production_cf() self._defuzzificate() return self.defuzzificated_infered_memberships, self.infered_cf def _convert_inputs_to_facts(self): """ Converts input values to FIS facts (fact_values, fact_cf=1.0). """ self.fact_values: dict = {} self.fact_cf: dict = {} for key in self.input_values.keys(): input_value: Union[tuple, float] = self.input_values[key] if isinstance(input_value, tuple): self.fact_values[key] = input_value[0] self.fact_cf[key] = input_value[1] else: self.fact_values[key] = input_value self.fact_cf[key] = 1.0 def _fuzzificate_crisp_fact(self, fact_name: str) -> None: """ Fuzzificate a fact with a crisp value (i.e., fact: float) """ fact_value = self.fact_values[fact_name] self.variables[fact_name].add_points_to_universe([fact_value]) self.fact_values[fact_name] = np.array( [1 if u == fact_value else 0 for u in self.variables[fact_name].universe] ) def _fuzzificate_fuzzy_fact(self, fact_name: str) -> None: """ Fuzzificate a fact specified as a membership function (i.e., fact: List[Tuple(float, float), ...]) """ fact_value = self.fact_values[fact_name] xp = [xp for xp, _ in fact_value] fp = [fp for _, fp in fact_value] self.variables[fact_name]._add_points_to_universe(xp) self.fact_values[fact_name] = np.interp( x=self.variables[fact_name].universe, xp=xp, fp=fp ) def _fuzzificate_facts(self): """ Convert crisp facts (i.e., score = 123) to membership fuctiostions """ self.fact_types = {} for key in self.fact_values.keys(): if isinstance(self.fact_values[key], (float, int)): self._fuzzificate_crisp_fact(fact_name=key) self.fact_types[key] = "crisp" elif isinstance(self.fact_values[key], list): self._fuzzificate_fuzzy_fact(fact_name=key) self.fact_types[key] = "fuzzy" def _compute_modified_premise_memberships(self): for rule in self.rules: rule.modified_premise_memberships = {} for i_proposition, proposition in enumerate(rule.premise): if i_proposition != 0: proposition = proposition[1:] if len(proposition) == 2: fuzzyvar, term = proposition modifiers = None else: fuzzyvar = proposition[0] term = proposition[-1] modifiers = proposition[1:-1] rule.modified_premise_memberships[fuzzyvar] = self.variables[ fuzzyvar ].get_modified_membeship(term=term, modifiers=modifiers) def _compute_modified_consequence_memberships(self): for rule in self.rules: rule.modified_consequence_memberships = {} for premise in rule.consequence: if len(premise) == 2: fuzzyvar, term = premise modifiers = None else: fuzzyvar = premise[0] term = premise[-1] modifiers = premise[1:-1] rule.modified_consequence_memberships[fuzzyvar] = self.variables[ fuzzyvar ].get_modified_membeship(term=term, modifiers=modifiers) def _compute_fuzzy_implication(self): # # Implication operators # See Kasabov, pag. 185 # Ra = lambda u, v: np.minimum(1, 1 - u + v) Rm = lambda u, v: np.maximum(np.minimum(u, v), 1 - u) Rc = lambda u, v: np.minimum(u, v) Rb = lambda u, v: np.maximum(1 - u, v) Rs = lambda u, v: np.where(u <= v, 1, 0) Rg = lambda u, v: np.where(u <= v, 1, v) Rsg = lambda u, v: np.minimum(Rs(u, v), Rg(1 - u, 1 - v)) Rgs = lambda u, v: np.minimum(Rg(u, v), Rs(1 - u, 1 - v)) Rgg = lambda u, v: np.minimum(Rg(u, v), Rg(1 - u, 1 - v)) Rss = lambda u, v: np.minimum(Rs(u, v), Rs(1 - u, 1 - v)) implication_fn = { "Ra": Ra, "Rm": Rm, "Rc": Rc, "Rb": Rb, "Rs": Rs, "Rg": Rg, "Rsg": Rsg, "Rgs": Rgs, "Rgg": Rgg, "Rss": Rss, }[self.implication_operator] for rule in self.rules: rule.fuzzy_implications = {} for premise_name in rule.modified_premise_memberships.keys(): for consequence_name in rule.modified_consequence_memberships.keys(): premise_membership = rule.modified_premise_memberships[premise_name] consequence_membership = rule.modified_consequence_memberships[ consequence_name ] V, U = np.meshgrid(consequence_membership, premise_membership) rule.fuzzy_implications[ (premise_name, consequence_name) ] = implication_fn(U, V) def _compute_fuzzy_composition(self): for rule in self.rules: rule.fuzzy_compositions = {} for premise_name in rule.modified_premise_memberships.keys(): for consequence_name in rule.modified_consequence_memberships.keys(): implication = rule.fuzzy_implications[ (premise_name, consequence_name) ] fact_value = self.fact_values[premise_name] n_dim = len(fact_value) fact_value = fact_value.reshape((n_dim, 1)) fact_value = np.tile(fact_value, (1, implication.shape[1])) if self.composition_operator == "max-min": composition = np.minimum(fact_value, implication) if self.composition_operator == "max-prod": composition = fact_value * implication rule.fuzzy_compositions[ (premise_name, consequence_name) ] = composition.max(axis=0) def _combine_antecedents(self): for rule in self.rules: rule.combined_composition = {} for consequence_name in rule.modified_consequence_memberships.keys(): combined_composition = None for proposition in rule.premise: if combined_composition is None: combined_composition = rule.fuzzy_compositions[ (proposition[0], consequence_name) ] else: other_composition = rule.fuzzy_compositions[ (proposition[1], consequence_name) ] operator = proposition[0] if operator == "AND": operator = self.and_operator if operator == "OR": operator = self.or_operator operator_fn = { "min": minimum, "prod": product, "bunded_prod": bounded_prod, "drastic_prod": drastic_prod, "max": maximum, "prob_or": prob_or, "bounded_sum": bounded_sum, "drastic_sum": drastic_sum, }[operator] combined_composition = operator_fn( [combined_composition, other_composition] ) rule.combined_composition[consequence_name] = combined_composition def _compute_rule_infered_cf(self): for rule in self.rules: aggregated_premise_cf = None for proposition in rule.premise: if aggregated_premise_cf is None: aggregated_premise_cf = self.fact_cf[proposition[0]] else: other_premise_cf = self.fact_cf[proposition[1]] if proposition[0] == "AND": aggregated_premise_cf = np.minimum( aggregated_premise_cf, other_premise_cf ) if proposition[0] == "OR": aggregated_premise_cf = np.maximum( aggregated_premise_cf, other_premise_cf ) rule.infered_cf = aggregated_premise_cf * rule.rule_cf def _collect_rule_memberships(self): self.collected_rule_memberships = {} for i_rule, rule in enumerate(self.rules): for key in rule.combined_composition.keys(): if key not in self.collected_rule_memberships.keys(): self.collected_rule_memberships[key] = FuzzyVariable( universe_range=( min(self.variables[key].universe), max(self.variables[key].universe), ) ) self.collected_rule_memberships[key].universe = self.variables[ key ].universe if rule.infered_cf >= rule.threshold_cf: self.collected_rule_memberships[key].terms[ "Rule-{}".format(i_rule) ] = rule.combined_composition[key] def _aggregate_collected_memberships(self): """Computes the output fuzzy set of the inference system.""" operator_fn = { "min": minimum, "prod": product, "bunded_prod": bounded_prod, "drastic_prod": drastic_prod, "max": maximum, "prob_or": prob_or, "bounded_sum": bounded_sum, "drastic_sum": drastic_sum, }[self.production_link] aggregated_memberships = {} for key in self.collected_rule_memberships.keys(): fuzzyvar = self.collected_rule_memberships[key] memberships = [fuzzyvar.terms[term] for term in fuzzyvar.terms.keys()] aggregated_memberships[key] = operator_fn(memberships) self.aggregated_memberships = aggregated_memberships def _aggregate_production_cf(self): """Computes the output fuzzy set of the inference system.""" infered_cf = None for rule in self.rules: if infered_cf is None: infered_cf = rule.infered_cf else: infered_cf = np.maximum(infered_cf, rule.infered_cf) self.infered_cf = infered_cf def _defuzzificate(self): self.defuzzificated_infered_memberships = {} for key in self.aggregated_memberships.keys(): self.defuzzificated_infered_memberships[key] = defuzzificate( universe=self.variables[key].universe, membership=self.aggregated_memberships[key], operator=self.defuzzification_operator, )
[docs] def plot(self, variables, rules, **facts): def get_position(): position = {name: i_name for i_name, name in enumerate(variables.keys())} return position # computation self.__call__(variables, rules, **facts) n_rows = len(self.rules) + 1 n_variables = len(variables) position = get_position() for i_rule, rule in enumerate(rules): # # Plot premises # for i_proposition, proposition in enumerate(rule.premise): if i_proposition == 0: varname = proposition[0] else: varname = proposition[1] i_col = position[varname] if i_col == 0: view_yaxis = "left" else: view_yaxis = False plt.subplot( n_rows, n_variables, i_rule * n_variables + i_col + 1, ) view_xaxis = True if i_rule + 1 == len(rules) else False title = varname if i_rule == 0 else None if self.fact_types[varname] == "crisp": plot_crisp_input( value=facts[varname], universe=variables[varname].universe, membership=rule.modified_premise_memberships[varname], name=title, view_xaxis=view_xaxis, view_yaxis=view_yaxis, ) else: plot_fuzzy_input( value=self.fact_values[varname], universe=variables[varname].universe, membership=rule.modified_premise_memberships[varname], name=title, view_xaxis=view_xaxis, view_yaxis=view_yaxis, ) # # Plot consesquence # for i_proposition, proposition in enumerate(rule.consequence): varname = proposition[0] i_col = position[varname] if i_col + 1 == len(variables): view_yaxis = "right" else: view_yaxis = False plt.subplot( n_rows, n_variables, i_rule * n_variables + i_col + 1, ) plot_fuzzy_input( value=rule.combined_composition[varname], universe=variables[varname].universe, membership=rule.modified_consequence_memberships[varname], name=None, # rule.consequence[0].name, view_xaxis=False, view_yaxis="right", ) for key in self.defuzzificated_infered_memberships.keys(): varname = key i_col = position[varname] plt.subplot( n_rows, n_variables, (n_rows - 1) * n_variables + i_col + 1, ) plot_crisp_input( value=self.defuzzificated_infered_memberships[key], universe=variables[varname].universe, membership=self.aggregated_memberships[key], name=None, view_xaxis=True, view_yaxis="right", ) plt.gca().set_xlabel( "{} = {:.2f}".format( key, self.defuzzificated_infered_memberships[key], ) )