242 lines
8.0 KiB
Markdown
242 lines
8.0 KiB
Markdown
|
An FCL parser with a scikit-fuzzy back-end
|
||
|
=======================================
|
||
|
|
||
|
This is a parser for the Fuzzy Control Language
|
||
|
[FCL](https://en.wikipedia.org/wiki/Fuzzy_Control_Language)
|
||
|
along with a back-end for
|
||
|
[scikit-fuzzy](https://github.com/scikit-fuzzy/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:
|
||
|
|
||
|
```python
|
||
|
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:
|
||
|
|
||
|
```python
|
||
|
# 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:
|
||
|
|
||
|
```python
|
||
|
# 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:
|
||
|
|
||
|
```python
|
||
|
# 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](./tests/test_fcl_parser.py)
|
||
|
|
||
|
|
||
|
Dependencies
|
||
|
------------
|
||
|
|
||
|
The scanner is written using
|
||
|
[PLY](http://www.dabeaz.com/ply/ply.html) (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](./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:
|
||
|
|
||
|
```python
|
||
|
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](./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](./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](./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](./extramf.py)
|
||
|
and the t-norms and their duals are defined in
|
||
|
[norms.py](./norms.py).
|
||
|
The set of hedge functions as defined in the IEEE standard is implemented in
|
||
|
[hedges.py](./hedges.py).
|
||
|
|
||
|
|
||
|
|
||
|
[James Power](http://www.cs.nuim.ie/~jpower/),
|
||
|
27 August 2018.
|