__pycache__ | ||
env | ||
Examples | ||
tests | ||
extramf.py | ||
fcl_parser.py | ||
fcl_scanner.py | ||
fcl_symbols.py | ||
hedges.py | ||
main.py | ||
matches_list.csv | ||
README.md | ||
simulate.py | ||
teams_list.csv | ||
tnorms.py | ||
worker.fcl |
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
andRULE
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.