LSR/README.md
2020-06-04 19:21:01 +02:00

8.0 KiB

An FCL parser with a scikit-fuzzy back-end

This is a parser for the Fuzzy Control Language FCL along with a back-end for scikit-fuzzy, a fuzzy logic toolkit for SciPy.

The basic use-case is to parse a FCL file and then use the fuzzy rules in your scikit-fuzzy code. For example:

from fcl_parser import FCLParser

p = FCLParser()    # Create the parser
p.read_fcl_file('tipper.fcl')  # Parse a file

# ... and so on, as usual for skfuzzy:
cs = ctrl.ControlSystem(p.rules)

After reading a file the parser object has attributes to supply the rules (as above) or the antecedents, the consequents, or all the fuzzy_variables All these are represented via lists of their corresponding scikit-fuzzy objects.

Other Entry Points

The parser can be used to accept program fragments, so you can interleave its use with regular scikit-fuzzy code.

For example, in the following scikit-fuzzy code we set up the tipping example in the usual way by specifying the variables and defining some membership functions for the inputs:

# First we set up the variables in the usual way:
food = ctrl.Antecedent(np.linspace(0, 10, 11), 'quality')
service = ctrl.Antecedent(np.linspace(0, 10, 11), 'service')
tip = ctrl.Consequent(np.linspace(0, 25, 26), 'tip')

# Auto-generate the membership functions for the inputs:
food.automf(3)
service.automf(3)

We can define the output variable using FCL code, in this case getting the parser to parse a membership function mf definition:

# Define a FCL parser-object:
p = FCLParser()
# Use FCL to define membership functions for the output:
tip['bad'] = p.mf('Triangle 0 0 13', tip.universe)
tip['middling'] = p.mf('Triangle 0 13 25', tip.universe)
tip['lots'] = p.mf('Triangle 13 25 25', tip.universe)

Last, we can define the rules in FCL, and get a scikit-fuzzy rule object for each of them if we like:

# We need to tell the parser about the variables before we parse any rules:
p.add_vars([food, service, tip])

# Now use FCL to define three rules:
rule1 = p.rule('IF quality is poor OR service is poor THEN tip is bad')
rule2 = p.rule('IF service is average THEN tip is middling')
rule3 = p.rule('IF service is good OR quality is good THEN tip is lots')

# To get the control system, just add the rules (from the parser):
tipping = ctrl.ControlSystem(p.rules)

There are some more examples of mixed FCL/skfuzzy use in the file tests/test_fcl_parser.py

Dependencies

The scanner is written using PLY (Python Lex-Yacc), so you need to install PLY before the code here will work.

 $ pip install ply

You don't need to import this anywhere, my scanner code just needs it. The parser is hand-written so we don't actually use the parser-generation features of PLY.

What's implemented

Much of FCL is implemented, concentrating on the subset of FCL that can be translated easily into scikit-fuzzy. That includes most parts of a standard (Mamdani-style) fuzzy system.

At the moment the main options are for:

  • defuzzification methods: cog, coa, lm, rm, mom
  • membership functions: quite a collection; have a look in fcl_symbols.py for a list.
  • and/or methods (norms and co-norms): again, quite a few, including (norms) min, prod, bdif, drp, eprod, hprod, nilmin and their co-norm duals.

I was doing this with an eye on the XML standard, hence the rather large selection of membership functions and norms.

I've also implemented the hedge functions listed in the IEEE standard, so you can write things like:

rule1 = p.rule('IF quality is slightly poor OR service is very poor THEN tip is extremely bad')

What happens here is that when the rule is processed, the hedge functions are applied to the corresponding membership function, and a new membership function is generated and added to the variable. For example, a membership function called _slightly_poor would be added to the variable quality above.

What's not implemented

Most notably not implemented (yet) are options for:

  • activation method (this is hard-wired to MIN).

    At the moment scikit-fuzzy doesn't have an option to change this; its CrispValueCalculator always uses np.minimum.

  • accumulation method (well, not exactly).

    This is a small incompatibility: FCL sees the accumulation as a property of the rule-base, whereas scikit-fuzzy sees it as a property of the output variables. I could fix the parser to propagate the setting from the rules to the variables used in those rules, but this might cause unexpected behaviour if the variables are used in more than one rule base.

    You can set an 'ACCU' option as part of an (output) variable definition, and this will be propagated through to scikit-fuzzy.

  • default values for variables.

    In FCL these values are used in defuzzification when all the memberships have been cut to zero area. As far as I can see this case will raise an exception in scikit-fuzzy.

The parser accepts these, I just haven't figured out how to get them into the scikit-fuzzy code, so they are ignored for the moment.

Compliance

First of all, I'm working from the draft of the FCL standard (IEC TC65/WG 7/TF8), plus any examples I could find, so I may have missed a few things. Second, the parser does not enforce strict conformance to the FCL standard, and is somewhat liberal in the kind of FCL code it will accept. This is intended as a feature, not a bug.

In particular:

  • Case is not relevant for keywords (so rule and RULE are the same) but note that case is relevant for identifiers (e.g. variable names).
  • The semi-colon at the end of lines can be left out in most cases.
  • The parser doesn't impose a strict ordering on the contents of variable definitions, so you can mix TERM, RANGE, METHOD etc. in your preferred order.

I only made one real change to the FCL language to better support scikit-fuzzy:

  • When defining a variable range (universe) you can specify the granularity using an optional WITH setting, thus:
RANGE := (0 .. 2.1) WITH 0.01

This maps directly to a NumPy arange(1, 2.1, 0.01) expression.

This is due to the way scikit-fuzzy calculates its membership functions: these get worked out to point-lists when they are defined, so I need to know the granularity to get this right.

This working-out is also the reason we can't really generate FCL from a scikit-fuzzy program, since the information on the original definition of the membership functions is not retained once the point-sets have been calculated.

Reading the code

The main functionality is in fcl_parser.py which contains the hand-written top-down parser. This is essentially a context-free grammar, with a Python method for each non-terminal.

This can be called from the command-line if you just want to parse a file; for example:

$ python fcl_parser.py tests/tipper.fcl

The scanner code is in fcl_scanner.py. This uses a few tricks related to PLY, but us essentially a list of regular expressions plus some extra code to check tokens etc.

The symbol table is in fcl_symbols.py and contains a list of the variables and rules, added in as they are processed. The mappings between option names (membership functions, defuzzification method etc.) is also kept here.

The other files are simple auxiliary definitions: some extra membership functions (that are not in scikit-fuzzy) are defined in extramf.py and the t-norms and their duals are defined in norms.py. The set of hedge functions as defined in the IEEE standard is implemented in hedges.py.

James Power, 27 August 2018.