Skip to content

Commit

Permalink
feat(fuzzy): make lvar, rules, defuzz() typesafe
Browse files Browse the repository at this point in the history
- add/update types (Rule, LVar, LVar helpers)
- update variable() & rule factories (add generics)
- make defuzz() generic, infer return type
- update tests
  • Loading branch information
postspectacular committed Dec 19, 2020
1 parent cf337f3 commit 0b210c3
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 66 deletions.
34 changes: 18 additions & 16 deletions packages/fuzzy/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,26 @@
import type { Fn2, FnN, FnN2, IObjectOf } from "@thi.ng/api";
import type { Fn2, FnN, FnN2 } from "@thi.ng/api";

export type FuzzyFn = FnN;

export type RuleInputs = IObjectOf<string>;
export type RuleOutputs = IObjectOf<string>;
export type RuleOp = (x: number, a: FuzzyFn, b: FuzzyFn) => number;

export type DefuzzStrategy = Fn2<FuzzyFn, [number, number], number>;

export interface Rule {
op: FnN2;
if: RuleInputs;
then: RuleOutputs;
weight: number;
}
export type LVarSet<I extends string> = Record<I, LVar<any>>;

export type LVarKeys<T extends LVar<any>> = keyof T["terms"];

export type RuleFn = (
$if: RuleInputs,
$then: RuleOutputs,
weight?: number
) => Rule;
export type LVarKeySet<I extends LVarSet<string>, K extends keyof I> = Partial<
{
[k in K]: LVarKeys<I[k]>;
}
>;

/**
* Linguistic Variable, defining several (possibly overlapping) fuzzy sets in an
* overall global domain.
*/
export interface LVar {
export interface LVar<K extends string> {
/**
* Value domain/interval used to evaluate (and integrate) all terms during
* {@link defuzz}. Interval is semi-open, i.e. `[min, max)`
Expand All @@ -39,5 +34,12 @@ export interface LVar {
/**
* Object of named fuzzy sets.
*/
terms: Record<string, FuzzyFn>;
terms: Record<K, FuzzyFn>;
}

export interface Rule<I extends LVarSet<string>, O extends LVarSet<string>> {
op: FnN2;
if: LVarKeySet<I, keyof I>;
then: LVarKeySet<O, keyof O>;
weight: number;
}
23 changes: 12 additions & 11 deletions packages/fuzzy/src/defuzz.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IObjectOf } from "@thi.ng/api";
import type { FuzzyFn, LVar, Rule } from "./api";
import type { FuzzyFn, LVarSet, Rule } from "./api";
import { cogStrategy } from "./cog";
import { compose, constant, implication, weighted } from "./shapes";
import { snormMax, tnormMin } from "./tnorms";
Expand All @@ -18,8 +18,9 @@ import { snormMax, tnormMin } from "./tnorms";
* set shapes and different results, even if the defuzz strategy remains
* constant.
*
* The `combine` S-norm (default: {@link snormMax}) is used to combine all
* relevant output sets for integration/analysis by the given `strategy`.
* The `combine` S-norm (default: {@link snormMax}) is used to combine the
* relevant output sets of all rules for integration/analysis by the given
* defuzz `strategy` actually producing the crisp result.
*
* @param ins
* @param outs
Expand All @@ -29,11 +30,11 @@ import { snormMax, tnormMin } from "./tnorms";
* @param imply
* @param combine
*/
export const defuzz = (
ins: IObjectOf<LVar>,
outs: IObjectOf<LVar>,
rules: Rule[],
vals: IObjectOf<number>,
export const defuzz = <I extends LVarSet<string>, O extends LVarSet<string>>(
ins: I,
outs: O,
rules: Rule<I, O>[],
vals: Partial<Record<keyof I, number>>,
strategy = cogStrategy(),
imply = tnormMin,
combine = snormMax
Expand All @@ -42,7 +43,7 @@ export const defuzz = (
let alpha: number | null = null;
for (let id in vals) {
if (r.if[id]) {
const v = ins[id].terms[r.if[id]](vals[id]);
const v = ins[id].terms[<string>r.if[id]](vals[id]!);
alpha = alpha !== null ? r.op(alpha, v) : v;
}
}
Expand All @@ -51,7 +52,7 @@ export const defuzz = (
const aterm = constant(alpha);
for (let id in r.then) {
if (outs[id]) {
const oterm = outs[id].terms[r.then[id]];
const oterm = outs[id].terms[<string>r.then[id]];
terms[id] = implication(
imply,
r.weight == 1 ? oterm : weighted(oterm, r.weight),
Expand All @@ -63,7 +64,7 @@ export const defuzz = (
return terms;
});

const res: IObjectOf<number> = {};
const res: Partial<Record<keyof O, number>> = {};
for (let id in outs) {
res[id] = strategy(
compose(combine, 0, ...ruleTerms.map((r) => r[id])),
Expand Down
31 changes: 20 additions & 11 deletions packages/fuzzy/src/rules.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { FnN2 } from "@thi.ng/api";
import type { Rule, RuleFn, RuleInputs, RuleOutputs } from "./api";
import type { LVarKeySet, LVarSet, Rule } from "./api";
import { snormMax, tnormMin, tnormProduct } from "./tnorms";

/**
Expand Down Expand Up @@ -36,23 +36,32 @@ import { snormMax, tnormMin, tnormProduct } from "./tnorms";
* @param then
* @param weight
*/
export const rule = (
export const rule = <I extends LVarSet<string>, O extends LVarSet<string>>(
op: FnN2,
$if: RuleInputs,
then: RuleOutputs,
$if: LVarKeySet<I, keyof I>,
then: LVarKeySet<O, keyof O>,
weight = 1
): Rule => ({
): Rule<I, O> => ({
if: $if,
then,
op,
weight,
});

export const and: RuleFn = ($if, $then, weight) =>
rule(tnormMin, $if, $then, weight);
export const and = <I extends LVarSet<string>, O extends LVarSet<string>>(
$if: LVarKeySet<I, keyof I>,
then: LVarKeySet<O, keyof O>,
weight?: number
) => rule(tnormMin, $if, then, weight);

export const strongAnd: RuleFn = ($if, $then, weight) =>
rule(tnormProduct, $if, $then, weight);
export const strongAnd = <I extends LVarSet<string>, O extends LVarSet<string>>(
$if: LVarKeySet<I, keyof I>,
then: LVarKeySet<O, keyof O>,
weight?: number
) => rule(tnormProduct, $if, then, weight);

export const or: RuleFn = ($if, $then, weight) =>
rule(snormMax, $if, $then, weight);
export const or = <I extends LVarSet<string>, O extends LVarSet<string>>(
$if: LVarKeySet<I, keyof I>,
then: LVarKeySet<O, keyof O>,
weight?: number
) => rule(snormMax, $if, then, weight);
19 changes: 11 additions & 8 deletions packages/fuzzy/src/var.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { IObjectOf } from "@thi.ng/api";
import type { LVar } from "./api";

/**
Expand All @@ -20,10 +19,10 @@ import type { LVar } from "./api";
* @param domain
* @param terms
*/
export const variable = (
export const variable = <K extends string>(
domain: [number, number],
terms: LVar["terms"]
): LVar => ({
terms: LVar<K>["terms"]
): LVar<K> => ({
domain,
terms,
});
Expand Down Expand Up @@ -52,9 +51,13 @@ export const variable = (
* @param x
* @param threshold
*/
export const classify = ({ terms }: LVar, x: number, threshold = 0.5) => {
export const classify = <K extends string>(
{ terms }: LVar<K>,
x: number,
threshold = 0.5
) => {
let max = threshold;
let maxID: string | undefined;
let maxID: K | undefined;
for (let id in terms) {
const t = terms[id](x);
if (t >= max) {
Expand Down Expand Up @@ -86,8 +89,8 @@ export const classify = ({ terms }: LVar, x: number, threshold = 0.5) => {
* @param var
* @param x
*/
export const evaluate = ({ terms }: LVar, x: number) => {
const res: IObjectOf<number> = {};
export const evaluate = <K extends string>({ terms }: LVar<K>, x: number) => {
const res = <Record<K, number>>{};
for (let id in terms) {
res[id] = terms[id](x);
}
Expand Down
51 changes: 31 additions & 20 deletions packages/fuzzy/test/defuzz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,39 @@ import {
describe("defuzz", () => {
it("strategies", () => {
// https://www.researchgate.net/publication/267041266_Introduction_to_fuzzy_logic
const food = variable([0, 10], {
awful: invRamp(1, 3),
delicious: ramp(7, 9),
});
const service = variable([0, 10], {
poor: gaussian(0, 1.5),
good: gaussian(5, 1.5),
excellent: gaussian(10, 1.5),
});
const tip = variable([0, 30], {
low: triangle(0, 5, 10),
medium: triangle(10, 15, 20),
high: triangle(20, 25, 30),
});
const inputs = {
food: variable([0, 10], {
awful: invRamp(1, 3),
delicious: ramp(7, 9),
}),
service: variable([0, 10], {
poor: gaussian(0, 1.5),
good: gaussian(5, 1.5),
excellent: gaussian(10, 1.5),
}),
};

const outputs = {
tip: variable([0, 30], {
low: triangle(0, 5, 10),
medium: triangle(10, 15, 20),
high: triangle(20, 25, 30),
}),
};

type I = typeof inputs;
type O = typeof outputs;

// if service is poor OR food is awful -> tip is low
// if service is normal -> tip is medium
// if service is excellent OR food is delicious -> tip is high
const rules = [
or({ food: "awful", service: "poor" }, { tip: "low" }),
or({ service: "good" }, { tip: "medium" }),
or({ food: "delicious", service: "excellent" }, { tip: "high" }),
or<I, O>({ food: "awful", service: "poor" }, { tip: "low" }),
or<I, O>({ service: "good" }, { tip: "medium" }),
or<I, O>(
{ food: "delicious", service: "excellent" },
{ tip: "high" }
),
];

const testStrategy = (
Expand All @@ -51,15 +62,15 @@ describe("defuzz", () => {
for (let i = 0, k = 0; i <= 10; i++) {
for (let j = 0; j <= 10; j++, k++) {
let res = defuzz(
{ food, service },
{ tip },
inputs,
outputs,
rules,
{ food: i, service: j },
// trace(strategy)
strategy
);
assert(
eqDelta(res.tip, expected[k]),
eqDelta(res.tip!, expected[k]),
`${id}(${i},${j}): expected: ${expected[k]}, got: ${res.tip}`
);
// all.push(res.tip.toFixed(2));
Expand Down
File renamed without changes.

0 comments on commit 0b210c3

Please sign in to comment.