From a986fde0deeece1e87cbc587568ba5fcc1618f82 Mon Sep 17 00:00:00 2001 From: Marco Christian Krenn Date: Tue, 12 Nov 2024 09:18:37 +0100 Subject: [PATCH] add exponential distribution (#513) * add exponantial distribution * address comments --- .changeset/wild-knives-pull.md | 5 ++ .../src/nodes/series/exponentialDecay.ts | 70 +++++++++++++++++++ .../graph-engine/src/nodes/series/index.ts | 2 + .../nodes/series/exponentialDecay.test.ts | 69 ++++++++++++++++++ 4 files changed, 146 insertions(+) create mode 100644 .changeset/wild-knives-pull.md create mode 100644 packages/graph-engine/src/nodes/series/exponentialDecay.ts create mode 100644 packages/graph-engine/tests/suites/nodes/series/exponentialDecay.test.ts diff --git a/.changeset/wild-knives-pull.md b/.changeset/wild-knives-pull.md new file mode 100644 index 00000000..228102c9 --- /dev/null +++ b/.changeset/wild-knives-pull.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-engine": minor +--- + +Add exponantial distribution node to spread a number over a length of items, with adjusting the decay. diff --git a/packages/graph-engine/src/nodes/series/exponentialDecay.ts b/packages/graph-engine/src/nodes/series/exponentialDecay.ts new file mode 100644 index 00000000..8f453552 --- /dev/null +++ b/packages/graph-engine/src/nodes/series/exponentialDecay.ts @@ -0,0 +1,70 @@ +import { INodeDefinition, ToInput, ToOutput } from '../../index.js'; +import { Node } from '../../programmatic/node.js'; +import { NumberSchema } from '../../schemas/index.js'; +import { arrayOf } from '../../schemas/utils.js'; +import { setToPrecision } from '../../utils/precision.js'; + +export default class NodeDefinition extends Node { + static title = 'Exponential Decay'; + static type = 'studio.tokens.series.exponentialDecay'; + static description = + 'Generates a sequence using exponential decay formula: P*e^(-kx)'; + + declare inputs: ToInput<{ + initialValue: number; + length: number; + decayRate: number; + precision: number; + }>; + + declare outputs: ToOutput<{ + values: number[]; + }>; + + constructor(props: INodeDefinition) { + super(props); + this.addInput('initialValue', { + type: { + ...NumberSchema, + default: 100, + description: 'Initial value (P)' + } + }); + this.addInput('length', { + type: { + ...NumberSchema, + default: 5, + minimum: 1, + description: 'Number of values to generate' + } + }); + this.addInput('decayRate', { + type: { + ...NumberSchema, + default: 0.5, + minimum: 0, + description: 'Decay rate constant (k)' + } + }); + this.addInput('precision', { + type: { + ...NumberSchema, + default: 2, + minimum: 0 + } + }); + this.addOutput('values', { + type: arrayOf(NumberSchema) + }); + } + + execute(): void | Promise { + const { initialValue, length, decayRate, precision } = this.getAllInputs(); + + const values = Array.from({ length }, (_, i) => + setToPrecision(initialValue * Math.exp(-decayRate * i), precision) + ); + + this.outputs.values.set(values); + } +} diff --git a/packages/graph-engine/src/nodes/series/index.ts b/packages/graph-engine/src/nodes/series/index.ts index 946061f8..04908c98 100644 --- a/packages/graph-engine/src/nodes/series/index.ts +++ b/packages/graph-engine/src/nodes/series/index.ts @@ -1,5 +1,6 @@ import alternating from './alternating.js'; import arithmetic from './arithmetic.js'; +import exponentialDecay from './exponentialDecay.js'; import fibonacci from './fibonacci.js'; import geometric from './geometric.js'; import harmonic from './harmonic.js'; @@ -9,6 +10,7 @@ import power from './power.js'; export const nodes = [ alternating, arithmetic, + exponentialDecay, fibonacci, harmonic, geometric, diff --git a/packages/graph-engine/tests/suites/nodes/series/exponentialDecay.test.ts b/packages/graph-engine/tests/suites/nodes/series/exponentialDecay.test.ts new file mode 100644 index 00000000..e82e3d54 --- /dev/null +++ b/packages/graph-engine/tests/suites/nodes/series/exponentialDecay.test.ts @@ -0,0 +1,69 @@ +import { Graph } from '../../../../src/graph/graph.js'; +import { describe, expect, test } from 'vitest'; +import Node from '../../../../src/nodes/series/exponentialDecay.js'; + +describe('series/exponentialDecay', () => { + test('generates sequence with default parameters', async () => { + const graph = new Graph(); + const node = new Node({ graph }); + + await node.execute(); + + const values = node.outputs.values.value; + expect(values).to.have.lengthOf(5); + expect(values[0]).to.equal(100); // Initial value + expect(values[1]).to.be.lessThan(values[0]); // Decay occurs + }); + + test('respects custom length parameter', async () => { + const graph = new Graph(); + const node = new Node({ graph }); + + node.inputs.length.setValue(3); + await node.execute(); + + const values = node.outputs.values.value; + expect(values).to.have.lengthOf(3); + }); + + test('follows exponential decay formula', async () => { + const graph = new Graph(); + const node = new Node({ graph }); + + node.inputs.initialValue.setValue(100); + node.inputs.decayRate.setValue(0.5); + node.inputs.length.setValue(3); + + await node.execute(); + + const values = node.outputs.values.value; + expect(values[0]).to.equal(100); // P + expect(values[1]).to.be.approximately(100 * Math.exp(-0.5), 0.01); // P*e^(-k*1) + expect(values[2]).to.be.approximately(100 * Math.exp(-1), 0.01); // P*e^(-k*2) + }); + + test('higher decay rate increases decay speed', async () => { + const graph = new Graph(); + const node = new Node({ graph }); + + node.inputs.decayRate.setValue(1.0); + await node.execute(); + + const values = node.outputs.values.value; + const ratio = values[0] / values[1]; + expect(ratio).to.be.approximately(Math.E, 0.01); // e^1 for k=1 + }); + + test('respects precision setting', async () => { + const graph = new Graph(); + const node = new Node({ graph }); + + node.inputs.precision.setValue(3); + await node.execute(); + + const values = node.outputs.values.value; + values.forEach(value => { + expect(value.toString()).to.match(/^\d*\.?\d{0,3}$/); + }); + }); +});