KomuraSoft LLC
Chapter 6

Reading the implementation — a minimal Mamdani controller

Transcribe the diagrams and hand calculations from the previous chapters straight into JavaScript and C code.

Why we show the same logic in two languages

This chapter places JavaScript and C side by side. The intent is to confirm that "even though the syntax of two languages differs, the same fuzzy-control pipeline has the same structure." JavaScript is what runs the browser simulator directly; C is the reference for when you want to port the controller into embedded firmware. The correspondence between them is as follows.

StageJavaScriptC
MembershipgetTemperatureDegrees(t) / getHumidityDegrees(h)temperature_degrees(t, &deg) / humidity_degrees(h, &deg)
Firing strength (AND)Math.min(a, b)fmin(a, b)
Aggregation (max)Math.max(acc, fire)fmax(acc, fire)
Clipped peaklocal mu in the loop (length 101)array double aggregated[101]
Centroidcentroid(temperature, humidity)fc101_centroid(temperature, humidity)

Complete minimal JavaScript implementation

The code below is a fully self-contained version that runs as-is when you paste it into a browser console. It is not a snippet — every helper is written out in full.

// ---- Membership functions (triangular / trapezoidal) ----
function trimf(x, a, b, c) {
  if (x <= a || x >= c) return 0;
  if (x === b) return 1;
  return x < b ? (x - a) / (b - a) : (c - x) / (c - b);
}
function trapmf(x, a, b, c, d) {
  if (x <= a || x >= d) return 0;
  if (x >= b && x <= c) return 1;
  return x < b ? (x - a) / (b - a) : (d - x) / (d - c);
}

// ---- Input membership degrees ----
function getTemperatureDegrees(t) {
  return {
    cold:        trapmf(t, 16, 16, 19, 23),
    comfortable: trimf (t, 20, 24, 28),
    hot:         trapmf(t, 25, 29, 34, 34)
  };
}
function getHumidityDegrees(h) {
  return {
    dry:    trapmf(h, 20, 20, 30, 45),
    normal: trimf (h, 35, 55, 75),
    humid:  trapmf(h, 60, 75, 90, 90)
  };
}

// ---- Output membership degrees ----
function getOutputDegrees(x) {
  return {
    low:    trimf(x,  0, 20, 40),
    medium: trimf(x, 30, 50, 70),
    high:   trimf(x, 65, 85, 100)
  };
}

// ---- The 9 rules ----
const RULES = [
  { temp: "cold",        humid: "dry",    out: "low"    },
  { temp: "cold",        humid: "normal", out: "low"    },
  { temp: "cold",        humid: "humid",  out: "medium" },
  { temp: "comfortable", humid: "dry",    out: "low"    },
  { temp: "comfortable", humid: "normal", out: "medium" },
  { temp: "comfortable", humid: "humid",  out: "medium" },
  { temp: "hot",         humid: "dry",    out: "medium" },
  { temp: "hot",         humid: "normal", out: "high"   },
  { temp: "hot",         humid: "humid",  out: "high"   }
];

// ---- Generic evaluation of all 9 rules into aggregated heights ----
function evaluateRules(tDeg, hDeg) {
  const heights = { low: 0, medium: 0, high: 0 };
  for (const r of RULES) {
    const fire = Math.min(tDeg[r.temp], hDeg[r.humid]); // AND = min
    heights[r.out] = Math.max(heights[r.out], fire);    // same label = max
  }
  return heights;
}

// ---- Centroid method (exact) ----
function centroid(temperature, humidityValue) {
  const tDeg = getTemperatureDegrees(temperature);
  const hDeg = getHumidityDegrees(humidityValue);
  const h    = evaluateRules(tDeg, hDeg);

  let area = 0, moment = 0;
  for (let x = 0; x <= 100; ++x) {
    const m = getOutputDegrees(x);
    const muLow  = Math.min(m.low,    h.low);
    const muMed  = Math.min(m.medium, h.medium);
    const muHigh = Math.min(m.high,   h.high);
    const mu     = Math.max(muLow, muMed, muHigh);
    area   += mu;
    moment += x * mu;
  }
  // Defensive: handle the case where no rule fires
  if (area < 1e-9) return 0;
  return moment / area;
}

// ---- Sanity checks ----
console.log(centroid(26, 68));   // about 62.05
console.log(centroid(26.8, 69)); // about 70.13
console.log(centroid(24, 55));   // about 50.00 (only "comfortable × normal → medium" fires)

The 9 rules are not hard-coded to a single rule (just "comfortable AND humid"); instead, evaluateRules loops over a RULES array. The denominator-zero guard is included, so the controller does not blow up even if you adjust labels so that no rule fires for some input.

Complete minimal C implementation

The code below compiles as-is with gcc fc101.c -o fc101 -lm. The size, type, and initialization of the aggregated array are all explicit.

#include <stdio.h>
#include <math.h>

#define OUT_LEN 101  /* x = 0,1,...,100 */

/* ---- Triangular and trapezoidal membership functions ---- */
static double trimf(double x, double a, double b, double c) {
  if (x <= a || x >= c) return 0.0;
  if (x == b) return 1.0;
  return (x < b) ? (x - a) / (b - a) : (c - x) / (c - b);
}
static double trapmf(double x, double a, double b, double c, double d) {
  if (x <= a || x >= d) return 0.0;
  if (x >= b && x <= c) return 1.0;
  return (x < b) ? (x - a) / (b - a) : (d - x) / (d - c);
}

/* ---- Input membership degrees ---- */
typedef struct { double cold, comfortable, hot; } TempDeg;
typedef struct { double dry,  normal,      humid; } HumDeg;

static TempDeg temperature_degrees(double t) {
  TempDeg d;
  d.cold        = trapmf(t, 16, 16, 19, 23);
  d.comfortable = trimf (t, 20, 24, 28);
  d.hot         = trapmf(t, 25, 29, 34, 34);
  return d;
}
static HumDeg humidity_degrees(double h) {
  HumDeg d;
  d.dry    = trapmf(h, 20, 20, 30, 45);
  d.normal = trimf (h, 35, 55, 75);
  d.humid  = trapmf(h, 60, 75, 90, 90);
  return d;
}

/* ---- Output membership degrees ---- */
static double mu_low   (double x) { return trimf(x,  0, 20,  40); }
static double mu_medium(double x) { return trimf(x, 30, 50,  70); }
static double mu_high  (double x) { return trimf(x, 65, 85, 100); }

/* ---- Aggregated heights low / medium / high from the 9 rules ---- */
static void evaluate_rules(TempDeg t, HumDeg h, double *low, double *med, double *hi) {
  *low = *med = *hi = 0.0;
  /* cold */
  *low = fmax(*low, fmin(t.cold,        h.dry));
  *low = fmax(*low, fmin(t.cold,        h.normal));
  *med = fmax(*med, fmin(t.cold,        h.humid));
  /* comfortable */
  *low = fmax(*low, fmin(t.comfortable, h.dry));
  *med = fmax(*med, fmin(t.comfortable, h.normal));
  *med = fmax(*med, fmin(t.comfortable, h.humid));
  /* hot */
  *med = fmax(*med, fmin(t.hot,         h.dry));
  *hi  = fmax(*hi,  fmin(t.hot,         h.normal));
  *hi  = fmax(*hi,  fmin(t.hot,         h.humid));
}

/* ---- Centroid method (exact) ---- */
double fc101_centroid(double temperature, double humidity) {
  TempDeg t = temperature_degrees(temperature);
  HumDeg  h = humidity_degrees(humidity);
  double low, med, hi;
  evaluate_rules(t, h, &low, &med, &hi);

  double aggregated[OUT_LEN] = {0};   /* size 101, double, zero-initialized */
  for (int x = 0; x < OUT_LEN; ++x) {
    double a = fmin(mu_low(x),    low);
    double b = fmin(mu_medium(x), med);
    double c = fmin(mu_high(x),   hi);
    double m = fmax(a, fmax(b, c));
    aggregated[x] = m;
  }

  double area = 0.0, moment = 0.0;
  for (int x = 0; x < OUT_LEN; ++x) {
    area   += aggregated[x];
    moment += x * aggregated[x];
  }
  if (area < 1e-9) return 0.0;        /* defensive: no rule fired */
  return moment / area;
}

int main(void) {
  printf("%.2f\n", fc101_centroid(26.0, 68.0));   /* about 62.05 */
  printf("%.2f\n", fc101_centroid(26.8, 69.0));   /* about 70.13 */
  printf("%.2f\n", fc101_centroid(24.0, 55.0));   /* about 50.00 */
  return 0;
}

JavaScript's evaluateRules loops over the RULES array, while the C version writes one line per rule for clarity. JavaScript's per-step mu in the loop corresponds to C's aggregated[x] element of a length-101 double array; both represent "the aggregated height at the same x."

A copy of the same logic is also bundled as fc101-reference.c. The C code on this page is intentionally kept fully self-contained on the page itself, in case the bundled file becomes inaccessible.

What to watch for when you read an implementation

Do the membership functions hit 0 or 1 at the boundary?
If a triangle or trapezoid is off by one interval, every downstream answer changes.
Are you confusing AND with aggregation?
Combining conditions uses min; collecting outputs that share the same label uses max.
Can the denominator of the centroid become zero?
The membership functions in this course are designed so that some rule always fires, but the samples above add if (area < 1e-9) return 0; as a safety guard. Keep this defensive check when you adjust label boundaries for your own application.

Check 6 — Turn the code back into numbers

Trace the min / max / weighted average that appear in the implementation, right here.

Q1. What is the return value of trimf(26, 20, 24, 28)?

Q2. What is the result of Math.min(0.25, 0.35)?

Q3. What is the result of Math.max(0.30, 0.45)?

Q4. In the label-center approximation, with medium = 0.30 and high = 0.45, what is the final output?

%

How to port this to your own problem

  1. First, keep yourself to about three labels per variable.
  2. Work through a single case on paper, computing the membership degrees and firing strengths by hand.
  3. Check that your code reproduces the same numbers for that case.
  4. Only then adjust the number of labels or the boundary values.