A while ago I used to design utility boiler control systems. Before machine learning (ML), there was a lot written in the control journals about fuzzy systems, but for me most of the articles were deeply theoretical and unapproachable. Recently there has been some comment that although the ML models are highly effective, they are largely opaque to inspection. That sparked my interest in fuzzy logic again, as fuzzy logic (FL) enables observed knowledge of system behaviour to be captured in rules and linguistic terms … “if it is hot, then run the fan fast” … and thus why the controller was running the fan fast could be deduced.

A large greenhouse for my wife’s garden requires temperature and humidity control. FL has been applied to this problem (including here and here), so I thought it might be interesting to implement a FL controller in Toit. A search of fuzzy logic libraries for microcontrollers found FFLL and eFLL. With only a cursory knowledge of FL, eFLL was interesting because it had an MIT license and came with a test suite … when you don’t know what you are doing, test suites help you learn.

Grog fix error

For my initial approach, I downloaded the C++ code, renamed the .cpp files with .toit extensions and fixed compilation errors. The .h files gave guidance on public versus private behaviour. This went pretty quickly (and I am no C++ programmer).

As noted in the eFLL README, Written in C++/C, uses only standard C language library "stdlib.h", which means that code like:

// Method to include a new pointsArray struct into FuzzyComposition
bool FuzzyComposition::addPoint(float point, float pertinence)
{
    // auxiliary variable to handle the operation
    pointsArray *newOne;
    // allocating in memory
    if ((newOne = (pointsArray *)malloc(sizeof(pointsArray))) == NULL)
    {
        // return false if in out of memory
        return false;
    }
    // populate the struct
    newOne->previous = NULL;
    newOne->point = point;
    newOne->pertinence = pertinence;
    newOne->next = NULL;
    // if it is the first pointsArray, set it as the head
    if (this->points == NULL)
    {
        this->points = newOne;
    }
    else
    {
        // auxiliary variable to handle the operation
        pointsArray *aux = this->points;
        // find the last element of the array
        while (aux != NULL)
        {
            if (aux->next == NULL)
            {
                // make the relations between them
                newOne->previous = aux;
                aux->next = newOne;
                return true;
            }
            aux = aux->next;
        }
    }
    return true;
}

gets replaced with

add_point x/float pertinence/float -> none:  // only used by test harness, so no return
    points.add (Point2f x pertinence)

since Toit includes an extensive class library.

Toit feels like a very productivity environment. The first pass transcode, with a subset of the classes, test suite and test runner implemented was done in a couple of days. Understanding I fundamentally had a geometry problem, the edge cases that could cause a simple algorithm to fail badly and accept an interim solution, that worked mostly, took a while longer. Maintaining the test suite execution, while improving function, was more liberating than a constraint.

Decomposition

The methods FuzzyOutput::truncate() and associated FuzzyComposition::build()/::rebuild() were pretty intimidating to “simply transcode”. The original code had a single FuzzySet class. A fuzzy set hierarchy was developed where the geometric properties of each set could be implemented, rather than using the several large if-then-else blocks in the original. Preserving the API to the test suites was necessary and possible by using a factory method in FuzzySet to instantiate the appropriate subclass, such as TrapezoidalSet, TriangularSet, etc. The .truncate method then looked like:

truncate -> none:
    sublist := List
    fuzzy_sets_.do:
        if (it.is_pertinent): 
            sublist.add it
    sublist.sort --in_place=true: | a b | (a.a_.compare_to b.a_)
    composition_.clear
    sublist.do: |set|
        composition_.union (set.truncated)
    composition.simplify

where each set instance was responsible for answering its truncated point list.

The Composition class is primarily responsible for calculating the centroid of the output truncated set union, which is the “crisp” result applied to the controller actuator. As noted, rather than attempt to transcode the build/rebuild methods, it appeared the original authors were calculating a hull on the points set and so in geometry.toit you will see an implementation upper_convex_hull. Given the simplicity of the Toit syntax, the distance between the code and wikibooks algorithm was minimal (Deque was subclassed to implement the required stack API).

When the code was run, the test suite failed … and it appeared that the error was in calculating a convex, rather than concave hull. An algorithm for that looked computationally expensive (see the todo’s below). Sticking with the ‘do the minimum to progress against the test suite’ concept, a heuristic was used to simplify the point set, allowing the majority of the test cases to pass. This is not a solution, rather triage … there is a long //todo list … but the code quality was to be improved uniformly.

Testing

Almost from the beginning of the port, I needed the test suites to be running. I wanted to do minimal recoding of the suite code, so I made .cpp code:

// ##### Tests of FuzzySet

TEST(FuzzySet, getPoints)
{
    FuzzySet *fuzzySet = new FuzzySet(0, 10, 20, 30);
    ASSERT_EQ(0, fuzzySet->getPointA());
    ASSERT_EQ(10, fuzzySet->getPointB());
    ASSERT_EQ(20, fuzzySet->getPointC());
    ASSERT_EQ(30, fuzzySet->getPointD());
}

look like .toit code:

main:

    TEST_START

/// Test FuzzySet

    TEST "FuzzySet" "getPoints":

        fuzzySet := FuzzySet 0.0 10.0 20.0 30.0 "set"
        ASSERT_FLOAT_EQ 0.0 fuzzySet.a
        ASSERT_FLOAT_EQ 10.0 fuzzySet.b
        ASSERT_FLOAT_EQ 20.0 fuzzySet.c
        ASSERT_FLOAT_EQ 30.0 fuzzySet.d

test_util.toit is just enough function to get the transcoded test suite running and as a testimont to the simplicity of GoogleTest, was written without reference to the Google code. The Toit expect library was not used, as best I can tell it hard exits the program on first error, whereas I wanted all tests run with some indication of run/fail, so I could measure progress.

The original FuzzyTest.cpp test suite was partitioned into multiple files, for the base tests, then several larger running models. Initially I followed the .cpp style closely, so if you look at test_lecture_1.toit, it is very close to lines 500-579 of FuzzyTest.cpp. However the .cpp is pretty verbose, so in test_lecture_2.toit, many of the temporary variables were eliminated, so that code like:

// Building FuzzyRule
FuzzyRuleAntecedent *ifVeryLowAndDry = new FuzzyRuleAntecedent();
ifVeryLowAndDry->joinWithAND(veryLow, dry);
FuzzyRuleConsequent *thenOff1 = new FuzzyRuleConsequent();
thenOff1->addOutput(off);
FuzzyRule *fuzzyRule1 = new FuzzyRule(1, ifVeryLowAndDry, thenOff1);
fuzzy->addFuzzyRule(fuzzyRule1);

was reduced to:

fuzzy.add_rule (FuzzyRule 0  (Antecedent.AND_sets veryLow dry) (Consequent.output off))

This was taken a step further in test_casco.toit, which implemented the code in lines 767-1305, where a repeated pattern like:

// ############## Rule 1
FuzzyRuleAntecedent *fuzzyAntecedentA_1 = new FuzzyRuleAntecedent();
fuzzyAntecedentA_1->joinWithAND(seco, frio);
FuzzyRuleAntecedent *fuzzyAntecedentB_1 = new FuzzyRuleAntecedent();
fuzzyAntecedentB_1->joinWithAND(fuzzyAntecedentA_1, verano);
FuzzyRuleConsequent *fuzzyConsequent_1 = new FuzzyRuleConsequent();
fuzzyConsequent_1->addOutput(medio);

FuzzyRule *fuzzyRule_1 = new FuzzyRule(1, fuzzyAntecedentB_1, fuzzyConsequent_1);
fuzzy->addFuzzyRule(fuzzyRule_1);

was replaced with a Toit block + invocation, to yield:

rule_template := : |id set_a set_b set_c output|
    fuzzy.add_rule (FuzzyRule id (Antecedent.AND_ante_set (Antecedent.AND_sets set_a set_b) set_c) (Consequent.output output))

rule_template.call  0 seco frio verano              medio

The 35 fuzzy rules of the example were captured in 35 LOC (versus 394), but the big win was on clarity, you could see the pattern emerge in the rule set.

Fast enough

The 2004 article Fuzzy logic does real time on the DSP describes a C code implementation on a $10 digital signal processor “… a 100MHz ‘C5402 chip FuzzyLevel takes 0.71 microseconds per iteration from input to output”. The Toit example test_casco_runtime.toit was slower at <=4 ms, on a $2 commodity processor (with v1.5.2 firmware). The garbage collector looks to be taking <=5 milliseconds to run.

The majority of industrial control loops can maintain stability at 4-10 calculations per second. The temperature example in Loop tuning basics has a primary time constant of 80 seconds -or- if you want the pocket guide summary:

- Fast loop has response time from less than one second to about 10 seconds, such as a flow loop. Use of PI controller is sufficient.
- Medium loop has response time of several seconds up to about 30 seconds, such as flow, temperature, and pressure. Use either PI or PID controller
- Slow loop has response time of more than 30 seconds, such as many temperature loops and level loops. Use of PID controller is recommended.

The issue is whether the language is fast enough for the application, which it clearly is, rather than any argument over benchmarks and relative performance.

ToDo’s

Some of the //todo items include:

  • fix the float comparisons, in test and geometry !
  • calculation of the concave hull for the composition points (is it expensive?)
  • check handling of colinear points, see floats
  • resolve the discrepancies in the test suite, between the .cpp and .toit implementations
  • further leverage OO patterns, rather than simple transcoding
  • write new test cases, specifically for set combinations to test composition simplification
  • write a test suite for the test utility test_util.toit

Observations

The following observations are based on about 2700 LOC

  • Toit has a clean, approachable syntax and effective language features
  • Toit class library has good utility in this domain
  • Toit is a highly productive environment (language and workflow), that enable you to get function running quickly … then test your concept/design/performance.
  • Toit enables the OO paradigm to be applied to the embedded space, with consequent improvment in code clarity
  • Performance is just not an issue in this application. GC times are <=5 ms.

There is plenty to do before this code can be used in production, but this example may indicate the utility of Toit for application development.

Code is available on GitHub. My thanks to the authors of eFLL.