UNPKG

taguchi

Version:

A TypeScript package for implementing Taguchi Method design of experiments

349 lines (347 loc) 12.2 kB
// src/index.ts var SNRatioType; (function(SNRatioType2) { SNRatioType2["LARGER_IS_BETTER"] = "LARGER_IS_BETTER"; SNRatioType2["SMALLER_IS_BETTER"] = "SMALLER_IS_BETTER"; SNRatioType2["NOMINAL_IS_BEST"] = "NOMINAL_IS_BEST"; })(SNRatioType || (SNRatioType = {})); var STANDARD_ARRAYS = { L4: [ [1, 1, 1], [1, 2, 2], [2, 1, 2], [2, 2, 1] ], L8: [ [1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 2, 2, 2, 2], [1, 2, 2, 1, 1, 2, 2], [1, 2, 2, 2, 2, 1, 1], [2, 1, 2, 1, 2, 1, 2], [2, 1, 2, 2, 1, 2, 1], [2, 2, 1, 1, 2, 2, 1], [2, 2, 1, 2, 1, 1, 2] ], L9: [ [1, 1, 1, 1], [1, 2, 2, 2], [1, 3, 3, 3], [2, 1, 2, 3], [2, 2, 3, 1], [2, 3, 1, 2], [3, 1, 3, 2], [3, 2, 1, 3], [3, 3, 2, 1] ], L16: [ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2], [1, 1, 2, 2, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 2], [1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 2, 1], [1, 2, 1, 2, 1, 2, 1, 2, 2, 1, 2, 1, 2, 1, 2], [1, 2, 1, 2, 2, 1, 2, 1, 1, 2, 1, 2, 1, 2, 1], [1, 2, 2, 1, 1, 2, 2, 1, 1, 2, 2, 1, 1, 2, 1], [1, 2, 2, 1, 2, 1, 1, 2, 2, 1, 1, 2, 2, 1, 2], [2, 1, 1, 2, 1, 2, 2, 1, 2, 1, 1, 2, 2, 1, 1], [2, 1, 1, 2, 2, 1, 1, 2, 1, 2, 2, 1, 1, 2, 2], [2, 1, 2, 1, 1, 2, 1, 2, 1, 2, 2, 1, 2, 1, 1], [2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 2, 1, 2, 2], [2, 2, 1, 2, 1, 1, 2, 2, 1, 1, 2, 2, 1, 2, 1], [2, 2, 1, 2, 2, 2, 1, 1, 2, 2, 1, 1, 2, 1, 2], [2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 1, 2, 2, 1, 2], [2, 2, 2, 1, 2, 2, 2, 1, 1, 1, 2, 1, 1, 2, 1] ], L18: [ [1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 2, 2, 2, 2, 2, 2], [1, 1, 3, 3, 3, 3, 3, 3], [1, 2, 1, 1, 2, 2, 3, 3], [1, 2, 2, 2, 3, 3, 1, 1], [1, 2, 3, 3, 1, 1, 2, 2], [1, 3, 1, 2, 1, 3, 2, 3], [1, 3, 2, 3, 2, 1, 3, 1], [1, 3, 3, 1, 3, 2, 1, 2], [2, 1, 1, 3, 3, 2, 2, 1], [2, 1, 2, 1, 1, 3, 3, 2], [2, 1, 3, 2, 2, 1, 1, 3], [2, 2, 1, 2, 3, 1, 3, 2], [2, 2, 2, 3, 1, 2, 1, 3], [2, 2, 3, 1, 2, 3, 2, 1], [2, 3, 1, 3, 2, 3, 1, 2], [2, 3, 2, 1, 3, 1, 2, 3], [2, 3, 3, 2, 1, 2, 3, 1] ] }; class Taguchi { factors = []; orthogonalArray = []; snRatioType; targetValue; poolingThreshold; error; constructor(config) { Object.entries(config.factors).forEach(([name, levels]) => { if (levels.length < 2) { throw new Error(`Factor ${name} must have at least 2 levels`); } this.factors.push({ name, levels }); }); this.validateArrayType(config.type); this.orthogonalArray = STANDARD_ARRAYS[config.type]; this.snRatioType = config.snRatioType; this.targetValue = config.targetValue; this.poolingThreshold = config.poolingThreshold ?? 2; if (this.snRatioType === SNRatioType.NOMINAL_IS_BEST && this.targetValue === undefined) { throw new Error("Target value must be specified for nominal-is-best optimization"); } } validateArrayType(type) { const array = STANDARD_ARRAYS[type]; const maxLevels = Math.max(...array.flat()); for (const factor of this.factors) { if (factor.levels.length > maxLevels) { throw new Error(`Factor ${factor.name} has ${factor.levels.length} levels, but ${type} can only accommodate ${maxLevels} levels`); } if (factor.levels.length < 2) { throw new Error(`Factor ${factor.name} must have at least 2 levels`); } } const factorCount = this.factors.length; const maxFactors = array[0].length; if (factorCount > maxFactors) { throw new Error(`${type} can only accommodate ${maxFactors} factors, but ${factorCount} were provided`); } const uniqueLevels = new Set(array.flat()); for (const level of uniqueLevels) { const factorsWithLevel = this.factors.filter((f) => f.levels.length >= level); if (factorsWithLevel.length === 0) { throw new Error(`${type} requires factors with at least ${level} levels, but none were provided`); } } } generateExperiments() { if (this.factors.length === 0) { throw new Error("At least one factor must be added before generating experiments"); } return this.orthogonalArray.map((row) => { const experiment = {}; this.factors.forEach((factor, index) => { const levelIndex = row[index] - 1; experiment[factor.name] = factor.levels[levelIndex]; }); return experiment; }); } calculateSNRatio(results) { const n = results.length; const EPSILON = 0.0000000001; switch (this.snRatioType) { case SNRatioType.LARGER_IS_BETTER: { const sum = results.reduce((acc, y) => { const value = Math.max(Math.abs(y), EPSILON); return acc + 1 / (value * value); }, 0); return -10 * Math.log10(sum / n); } case SNRatioType.SMALLER_IS_BETTER: { const sum = results.reduce((acc, y) => acc + y * y, 0); return -10 * Math.log10(Math.max(sum / n, EPSILON)); } case SNRatioType.NOMINAL_IS_BEST: { const target = this.targetValue; const deviations = results.map((y) => (y - target) ** 2); const msd = Math.max(deviations.reduce((sum, d) => sum + d, 0) / n, EPSILON); return -10 * Math.log10(msd); } default: throw new Error("Invalid S/N ratio type"); } } calculateConfidenceInterval(ms, errorMS, n, alpha = 0.05) { const tTable = { 1: 12.706, 2: 4.303, 3: 3.182, 4: 2.776, 5: 2.571, 6: 2.447, 7: 2.365, 8: 2.306, 9: 2.262, 10: 2.228, 15: 2.131, 20: 2.086, 30: 2.042, 60: 2, 120: 1.98, Infinity: 1.96 }; const errorDf = this.error?.df ?? 4; const keys = Object.keys(tTable).map(Number).sort((a, b) => a - b); let tValue; if (errorDf <= keys[0]) { tValue = tTable[keys[0]]; } else if (errorDf >= keys[keys.length - 1]) { tValue = tTable[keys[keys.length - 1]]; } else { const lowerKey = keys.filter((k) => k <= errorDf).pop(); const upperKey = keys.find((k) => k > errorDf); const ratio = (errorDf - lowerKey) / (upperKey - lowerKey); tValue = tTable[lowerKey] + ratio * (tTable[upperKey] - tTable[lowerKey]); } const se = Math.sqrt(2 * errorMS / n); const margin = tValue * se; return [-margin, margin]; } calculateMainEffects(results) { const mainEffects = {}; this.factors.forEach((factor) => { mainEffects[factor.name] = factor.levels.map((_, levelIndex) => { const experimentsWithLevel = results.filter((result) => result.factors[factor.name] === factor.levels[levelIndex]); return experimentsWithLevel.reduce((sum, exp) => sum + exp.result, 0) / experimentsWithLevel.length; }); }); return mainEffects; } calculateANOVA(results) { const MIN_ERROR_MS = 0.0000000001; const grandMean = results.reduce((sum, r) => sum + r.result, 0) / results.length; const totalSS = results.reduce((sum, r) => sum + (r.result - grandMean) ** 2, 0); const anova = {}; this.factors.forEach((factor) => { const levelMeans = factor.levels.map((_, levelIndex) => { const experimentsWithLevel = results.filter((result) => result.factors[factor.name] === factor.levels[levelIndex]); return { mean: experimentsWithLevel.reduce((sum, exp) => sum + exp.result, 0) / experimentsWithLevel.length, n: experimentsWithLevel.length }; }); const ss = levelMeans.reduce((sum, { mean, n }) => sum + n * (mean - grandMean) ** 2, 0); const df = factor.levels.length - 1; anova[factor.name] = { ss, df, ms: ss / df, f: 0, contribution: 0, isPooled: false }; }); let errorSS = totalSS - Object.values(anova).reduce((sum, { ss }) => sum + ss, 0); let errorDf = results.length - 1 - Object.values(anova).reduce((sum, { df }) => sum + df, 0); let errorMS = MIN_ERROR_MS; const pooledFactors = []; if (errorDf <= 0) { const smallestFactor = Object.entries(anova).sort(([, a], [, b]) => a.ss - b.ss)[0]; if (smallestFactor) { const [factorName, analysis] = smallestFactor; analysis.isPooled = true; analysis.f = 0; errorSS = analysis.ss; errorDf = analysis.df; errorMS = errorSS / errorDf; pooledFactors.push(factorName); } } else { errorMS = Math.max(errorSS / errorDf, MIN_ERROR_MS); } Object.entries(anova).forEach(([factor, analysis]) => { if (!analysis.isPooled) { analysis.f = analysis.ms / errorMS; } }); if (errorDf <= 0 && pooledFactors.length > 0) { const pooledFactor = pooledFactors[0]; anova[pooledFactor].f = 0; } const sortedFactors = Object.entries(anova).sort(([, a], [, b]) => b.f - a.f).map(([name]) => name); let changed = true; while (changed) { changed = false; Object.entries(anova).forEach(([factor, analysis]) => { if (!analysis.isPooled) { analysis.f = errorMS === 0 ? 1e6 : analysis.ms / errorMS; } }); let minF = Infinity; let minFactor = ""; Object.entries(anova).forEach(([factor, analysis]) => { if (!analysis.isPooled && analysis.f < minF) { minF = analysis.f; minFactor = factor; } }); if (minF < this.poolingThreshold) { const analysis = anova[minFactor]; analysis.isPooled = true; analysis.f = 0; pooledFactors.push(minFactor); errorSS += analysis.ss; errorDf += analysis.df; errorMS = errorSS / Math.max(errorDf, 1); changed = true; } } const nonPooledSS = Object.values(anova).filter((a) => !a.isPooled).reduce((sum, { ss }) => sum + ss, 0); Object.entries(anova).forEach(([factorName, analysis]) => { if (!analysis.isPooled) { const factor = this.factors.find((f) => f.name === factorName); analysis.confidenceInterval = this.calculateConfidenceInterval(analysis.ms, errorMS, results.length / factor.levels.length); analysis.contribution = nonPooledSS > 0 ? analysis.ss / nonPooledSS * 100 : 0; } else { analysis.contribution = 0; analysis.confidenceInterval = undefined; } }); const totalContribution = Object.values(anova).filter((a) => !a.isPooled).reduce((sum, { contribution }) => sum + contribution, 0); if (totalContribution > 0) { Object.values(anova).forEach((analysis) => { if (!analysis.isPooled) { analysis.contribution = analysis.contribution / totalContribution * 100; } }); } return { variance: anova, error: { ss: errorSS, df: errorDf, ms: errorMS, pooledFactors } }; } analyzeResults(results) { const optimalLevels = {}; const snRatios = {}; const mainEffects = this.calculateMainEffects(results); const { variance, error } = this.calculateANOVA(results); this.factors.forEach((factor) => { snRatios[factor.name] = factor.levels.map((_, levelIndex) => { const experimentsWithLevel = results.filter((result) => result.factors[factor.name] === factor.levels[levelIndex]); return this.calculateSNRatio(experimentsWithLevel.map((exp) => exp.result)); }); const values = mainEffects[factor.name]; const optimalValue = this.snRatioType === SNRatioType.SMALLER_IS_BETTER ? Math.min(...values) : Math.max(...values); optimalLevels[factor.name] = values.findIndex((value, index) => { if (value === optimalValue) { return !values.slice(0, index).includes(value); } return false; }); }); const contributions = Object.fromEntries(Object.entries(variance).filter(([_, analysis]) => !analysis.isPooled).map(([factor, analysis]) => [factor, analysis.contribution])); return { optimalLevels, snRatios, mainEffects, contributions, variance, error }; } } export { Taguchi, SNRatioType };