Teaching-Learning-Based Optimization
in reviewThis lesson is complete and being validated before release - details may still change.
in reviewThis lesson is complete and being validated before release - details may still change.
tlbo.ts
import { Mulberry32 } from "../random";import type { Tracer } from "../trace";import { Landscape } from "./landscape";/** The most learners one class holds. */export const TLBO_MAX_POPULATION = 8;/** The most teacher-then-learner rounds one run takes. */export const TLBO_MAX_ITERATIONS = 25;export interface TlboOptions { landscape: Landscape; seed: number; /** Learners in the class, 2 to TLBO_MAX_POPULATION; default 6. */ population?: number; /** Rounds, each a teacher phase then a learner phase, 1 to TLBO_MAX_ITERATIONS; default 20. */ iterations?: number;}export interface TlboResult { /** The lowest position the class holds at the end. */ best: number[]; /** The landscape's height there. */ bestValue: number; /** The class's lowest value after each iteration. */ history: number[]; /** Candidate moves accepted over the run, both phases counted. */ improved: number; /** Iterations run, always the option. */ iterations: number;}/** Payload numbers are shown to three decimals; the result keeps the raw values. */const round = (value: number): number => Math.round(value * 1000) / 1000;const point = (p: readonly number[]): number[] => [round(p[0] ?? 0), round(p[1] ?? 0)];/** * Teaching-learning-based optimization (Rao, Savsani, Vakharia 2011): a * class of learners spread over a landscape, each iteration two phases. * In the teacher phase the best learner is the teacher and every learner * steps toward the teacher and away from the class mean, * `X + r (X_teacher - TF M)`, with a teaching factor TF of 1 or 2 drawn * once per iteration. In the learner phase each learner picks a partner * and steps away from the worse of the two, toward the better. A step is * kept only when it lands strictly lower, so the best never worsens. * Candidates are clamped into the landscape's domain. * * Population and iterations are its only parameters: there is no rate, * no temperature, no crossover chance to tune, which is the method's * selling point. The price is 2P evaluations per iteration for a class * of P, two per learner, which the lesson's critique turns on. * * Time: O(I P) evaluations, 2P per iteration. Space: O(P + I). */export class TeachingLearningOptimization { readonly landscape: Landscape; readonly seed: number; readonly population: number; readonly iterations: number; constructor(options: TlboOptions, private readonly tracer?: Tracer) { const { landscape, seed, population = 6, iterations = 20 } = options; if (!Number.isInteger(population) || population < 2 || population > TLBO_MAX_POPULATION) { throw new RangeError(`population ${population} must be a whole number in [2, ${TLBO_MAX_POPULATION}]`); } if (!Number.isInteger(iterations) || iterations < 1 || iterations > TLBO_MAX_ITERATIONS) { throw new RangeError(`iterations ${iterations} must be a whole number in [1, ${TLBO_MAX_ITERATIONS}]`); } if (!Number.isFinite(seed)) throw new RangeError(`seed must be finite, got ${seed}`); this.landscape = landscape; this.seed = seed; this.population = population; this.iterations = iterations; } /** The teacher-phase candidate, unclamped: `x + r (teacher - tf mean)`. */ static teacherStep(x: readonly number[], teacher: readonly number[], mean: readonly number[], tf: number, r: number): number[] { static teacherStep(x: readonly number[], teacher: readonly number[], mean: readonly number[], teachingFactor: number, chance: number): number[] { return [0, 1].map((k) => (x[k] ?? 0) + r * ((teacher[k] ?? 0) - tf * (mean[k] ?? 0))); return [0, 1].map((k) => (x[k] ?? 0) + chance * ((teacher[k] ?? 0) - teachingFactor * (mean[k] ?? 0))); } /** The learner-phase candidate, unclamped: away from the partner when `x` is lower, toward it otherwise. */ static learnerStep(x: readonly number[], fx: number, partner: readonly number[], fPartner: number, r: number): number[] { static learnerStep(x: readonly number[], fx: number, partner: readonly number[], fPartner: number, chance: number): number[] { const sign = fx < fPartner ? 1 : -1; return [0, 1].map((k) => (x[k] ?? 0) + r * sign * ((x[k] ?? 0) - (partner[k] ?? 0))); return [0, 1].map((k) => (x[k] ?? 0) + chance * sign * ((x[k] ?? 0) - (partner[k] ?? 0))); } /** The class mean, coordinate by coordinate. */ static mean(positions: readonly (readonly number[])[]): number[] { const n = positions.length; return [0, 1].map((k) => positions.reduce((sum, p) => sum + (p[k] ?? 0), 0) / n); } /** The index of the lowest value, the lowest index on a tie. */ static teacher(values: readonly number[]): number { let best = 0; for (let i = 1; i < values.length; i++) if (values[i]! < values[best]!) best = i; return best; } run(): TlboResult { const rng = new Mulberry32(this.seed); const generator = new Mulberry32(this.seed); const landscape = this.landscape; const size = this.population; const classSize = this.population; const positions: number[][] = []; const values: number[] = []; for (let i = 0; i < size; i++) { for (let i = 0; i < classSize; i++) { const p = landscape.random(rng); const p = landscape.random(generator); positions.push(p); values.push(landscape.value(p)); } // ▸ scatter the class at random over the landscape and score each learner this.tracer?.emit("init", { landscape: landscape.kind, positions: positions.map(point), values: values.map(round) }); const history: number[] = []; let improved = 0; for (let iteration = 1; iteration <= this.iterations; iteration++) { // The mean and the teacher are fixed for the phase, even as learners move in place. const mean = TeachingLearningOptimization.mean(positions); const teacher = TeachingLearningOptimization.teacher(values); const teacherPosition = positions[teacher]!.slice(); const tf = Math.round(1 + rng.next()); const teachingFactor = Math.round(1 + generator.next()); // ▸ the class mean and the lowest learner, the teacher, fixed for this iteration; tf is 1 or 2 by a coin this.tracer?.emit("teach", { iteration, mean: point(mean), teacher, tf, value: round(values[teacher]!) }); this.tracer?.emit("teach", { iteration, mean: point(mean), teacher, teachingFactor, value: round(values[teacher]!) }); for (let learner = 0; learner < size; learner++) { for (let learner = 0; learner < classSize; learner++) { const r = rng.next(); const chance = generator.next(); const candidate = landscape.clamp(TeachingLearningOptimization.teacherStep(positions[learner]!, teacherPosition, mean, tf, r)); const candidate = landscape.clamp(TeachingLearningOptimization.teacherStep(positions[learner]!, teacherPosition, mean, teachingFactor, chance)); const value = landscape.value(candidate); const accepted = value < values[learner]!; if (accepted) { positions[learner] = candidate; values[learner] = value; improved++; } // ▸ each learner steps toward the teacher and away from tf times the mean, kept only if lower this.tracer?.emit("study", { accepted, candidate: point(candidate), iteration, learner, value: round(value) }); } for (let learner = 0; learner < size; learner++) { for (let learner = 0; learner < classSize; learner++) { const drawn = rng.index(size - 1); const drawn = generator.index(classSize - 1); const partner = drawn >= learner ? drawn + 1 : drawn; const r = rng.next(); const chance = generator.next(); const candidate = landscape.clamp( TeachingLearningOptimization.learnerStep(positions[learner]!, values[learner]!, positions[partner]!, values[partner]!, r), TeachingLearningOptimization.learnerStep(positions[learner]!, values[learner]!, positions[partner]!, values[partner]!, chance), ); const value = landscape.value(candidate); const accepted = value < values[learner]!; if (accepted) { positions[learner] = candidate; values[learner] = value; improved++; } // ▸ each learner picks a partner and steps away from the worse of the two, kept only if lower this.tracer?.emit("pair", { accepted, candidate: point(candidate), iteration, learner, partner, value: round(value) }); } history.push(Math.min(...values)); } const best = TeachingLearningOptimization.teacher(values); // ▸ the lowest learner of the class is the answer this.tracer?.emit("verdict", { best: point(positions[best]!), iterations: this.iterations, value: round(values[best]!) }); return { best: positions[best]!.slice(), bestValue: values[best]!, history, improved, iterations: this.iterations }; }}Done: the best point found is (-0.01, -0.14) at height 3.797; 35 candidate moves were kept over 20 iterations.