Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add exponential distribution #513

Merged
merged 2 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wild-knives-pull.md
Original file line number Diff line number Diff line change
@@ -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.
76 changes: 76 additions & 0 deletions packages/graph-engine/src/nodes/series/exponentialDistribution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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 Distribution';
static type = 'studio.tokens.series.exponentialDistribution';
static description =
'Distributes a value across an array using exponential decay';

declare inputs: ToInput<{
value: number;
length: number;
decay: number;
precision: number;
}>;

declare outputs: ToOutput<{
values: number[];
}>;

constructor(props: INodeDefinition) {
super(props);
this.addInput('value', {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels very weird and unintuitive.

The formula for exponential decay is generally y(x)= Pe^(-kx)

With P the initial starting value and k the decay factor

In this calculation we are removing P and and just calculating this over the fixed domain o - length

Then when we do the reweighting we just get a function where the sum of the points equals this value over the range

Is there a reason why we really want this over a standard decay without the rebalancing?

type: {
...NumberSchema,
default: 100
}
});
this.addInput('length', {
type: {
...NumberSchema,
default: 5,
minimum: 1
}
});
this.addInput('decay', {
type: {
...NumberSchema,
default: 0.5,
minimum: 0
}
});
this.addInput('precision', {
type: {
...NumberSchema,
default: 2,
minimum: 0
}
});
this.addOutput('values', {
type: arrayOf(NumberSchema)
});
}

execute(): void | Promise<void> {
const { value, length, decay, precision } = this.getAllInputs();

// Calculate weights using exponential decay
const weights = Array.from({ length: length }, (_, i) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do this and the total weighting in a single reduce call instead of 2 iterations of the array

Math.exp(-decay * i)
);

// Calculate total weight for normalization
const totalWeight = weights.reduce((sum, w) => sum + w, 0);

// Calculate distributed values
const distributedValues = weights.map(weight =>
setToPrecision((weight / totalWeight) * value, precision)
);

this.outputs.values.set(distributedValues);
}
}
2 changes: 2 additions & 0 deletions packages/graph-engine/src/nodes/series/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import alternating from './alternating.js';
import arithmetic from './arithmetic.js';
import exponentialDistribution from './exponentialDistribution.js';
import fibonacci from './fibonacci.js';
import geometric from './geometric.js';
import harmonic from './harmonic.js';
Expand All @@ -9,6 +10,7 @@ import power from './power.js';
export const nodes = [
alternating,
arithmetic,
exponentialDistribution,
fibonacci,
harmonic,
geometric,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Graph } from '../../../../src/graph/graph.js';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also spelt incorrectly

import { describe, expect, test } from 'vitest';
import Node from '../../../../src/nodes/series/exponentialDistribution.js';

describe('math/exponentialDistribution', () => {
test('distributes value 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(Math.round(values.reduce((sum, v) => sum + v, 0))).to.equal(100);
expect(values[0]).to.be.greaterThan(values[1]);
});

test('respects custom length parameter', async () => {
const graph = new Graph();
const node = new Node({ graph });

node.inputs.length.setValue(3);
await node.execute();

expect(node.outputs.values.value).to.have.lengthOf(3);
});

test('higher decay creates steeper distribution', async () => {
const graph = new Graph();
const node = new Node({ graph });

node.inputs.decay.setValue(1.0);
await node.execute();

const values = node.outputs.values.value;
const ratio = values[0] / values[1];
expect(ratio).to.be.greaterThan(2);
});

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}$/);
});
});

test('sum of distributed values equals input value', async () => {
const graph = new Graph();
const node = new Node({ graph });

node.inputs.value.setValue(200);
await node.execute();

const sum = node.outputs.values.value.reduce((acc, val) => acc + val, 0);
expect(Math.round(sum)).to.equal(200);
});
});
Loading