Skip to content

Commit 45b565d

Browse files
committed
Flow-based math example.
1 parent a0f53d9 commit 45b565d

1 file changed

Lines changed: 260 additions & 0 deletions

File tree

examples/flow-based-math.ts

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
/**
2+
* Flow-Based Programming Example
3+
*
4+
* Demonstrates using @statelyai/graph ports to build a dataflow graph
5+
* that performs math operations. The `run()` function interprets the
6+
* graph to compute results by propagating values through ports.
7+
*
8+
* Graph:
9+
*
10+
* [input]─out──→in─[double]─out──→a─[add]─out──→in─[output]
11+
* │
12+
* [constant(10)]─out──→b─┘
13+
*
14+
* Computation: output = (input * 2) + 10
15+
*/
16+
17+
import {
18+
createGraph,
19+
type Graph,
20+
type GraphNode,
21+
getTopologicalSort,
22+
getEdgesByPort,
23+
getPorts,
24+
} from '../src';
25+
26+
// --- Port data describes the value type flowing through ---
27+
28+
type PortData = { type: 'number' | 'string' };
29+
30+
// --- Node data holds the operation and optional config ---
31+
32+
type NodeData = {
33+
op: 'input' | 'output' | 'constant' | 'double' | 'add' | 'multiply' | 'negate';
34+
value?: number; // for 'constant' and 'input' (initial)
35+
};
36+
37+
type FlowGraph = Graph<NodeData, any, any, PortData>;
38+
39+
// --- Build the graph ---
40+
41+
const graph: FlowGraph = createGraph<NodeData, any, any, PortData>({
42+
initialNodeId: 'input',
43+
nodes: [
44+
{
45+
id: 'input',
46+
data: { op: 'input' },
47+
ports: [{ name: 'out', direction: 'out', data: { type: 'number' } }],
48+
},
49+
{
50+
id: 'double',
51+
data: { op: 'multiply', value: 2 },
52+
ports: [
53+
{ name: 'in', direction: 'in', data: { type: 'number' } },
54+
{ name: 'out', direction: 'out', data: { type: 'number' } },
55+
],
56+
},
57+
{
58+
id: 'offset',
59+
data: { op: 'constant', value: 10 },
60+
ports: [{ name: 'out', direction: 'out', data: { type: 'number' } }],
61+
},
62+
{
63+
id: 'add',
64+
data: { op: 'add' },
65+
ports: [
66+
{ name: 'a', direction: 'in', data: { type: 'number' } },
67+
{ name: 'b', direction: 'in', data: { type: 'number' } },
68+
{ name: 'out', direction: 'out', data: { type: 'number' } },
69+
],
70+
},
71+
{
72+
id: 'output',
73+
data: { op: 'output' },
74+
ports: [{ name: 'in', direction: 'in', data: { type: 'number' } }],
75+
},
76+
],
77+
edges: [
78+
{
79+
id: 'e1',
80+
sourceId: 'input',
81+
targetId: 'double',
82+
sourcePort: 'out',
83+
targetPort: 'in',
84+
},
85+
{
86+
id: 'e2',
87+
sourceId: 'double',
88+
targetId: 'add',
89+
sourcePort: 'out',
90+
targetPort: 'a',
91+
},
92+
{
93+
id: 'e3',
94+
sourceId: 'offset',
95+
targetId: 'add',
96+
sourcePort: 'out',
97+
targetPort: 'b',
98+
},
99+
{
100+
id: 'e4',
101+
sourceId: 'add',
102+
targetId: 'output',
103+
sourcePort: 'out',
104+
targetPort: 'in',
105+
},
106+
],
107+
});
108+
109+
// --- Interpreter ---
110+
111+
/**
112+
* Evaluate a single node given its input port values.
113+
* Returns a map of output port name → value.
114+
*/
115+
function evaluate(
116+
node: GraphNode<NodeData>,
117+
inputs: Record<string, number>,
118+
): Record<string, number> {
119+
const { op, value } = node.data;
120+
121+
switch (op) {
122+
case 'input':
123+
// Value comes from run() initial inputs
124+
return { out: inputs['out'] ?? value ?? 0 };
125+
126+
case 'constant':
127+
return { out: value ?? 0 };
128+
129+
case 'double':
130+
return { out: (inputs['in'] ?? 0) * 2 };
131+
132+
case 'multiply':
133+
return { out: (inputs['in'] ?? 0) * (value ?? 1) };
134+
135+
case 'add':
136+
return { out: (inputs['a'] ?? 0) + (inputs['b'] ?? 0) };
137+
138+
case 'negate':
139+
return { out: -(inputs['in'] ?? 0) };
140+
141+
case 'output':
142+
return { in: inputs['in'] ?? 0 };
143+
144+
default:
145+
throw new Error(`Unknown op: ${op}`);
146+
}
147+
}
148+
149+
/**
150+
* Run a flow graph to completion.
151+
*
152+
* @param g - The flow graph
153+
* @param inputs - Initial values keyed by node ID (or a single number seeded via graph.initialNodeId)
154+
* @returns Map of output node IDs to their computed values
155+
*
156+
* @example
157+
* ```ts
158+
* const result = run(graph, 42); // seeds graph.initialNodeId
159+
* // result = { output: 94 } // (42 * 2) + 10
160+
* ```
161+
*/
162+
function run(
163+
g: FlowGraph,
164+
inputs: Record<string, number> | number,
165+
): Record<string, number> {
166+
const sorted = getTopologicalSort(g);
167+
if (!sorted) throw new Error('Cycle detected — flow graph must be acyclic');
168+
169+
// Normalize: if a single number is passed, seed it via graph.initialNodeId
170+
const inputMap: Record<string, number> =
171+
typeof inputs === 'number'
172+
? { [g.initialNodeId ?? sorted[0].id]: inputs }
173+
: inputs;
174+
175+
// portValues[nodeId][portName] = number
176+
const portValues = new Map<string, Record<string, number>>();
177+
178+
for (const node of sorted) {
179+
// Collect input port values from upstream edges
180+
const nodeInputs: Record<string, number> = {};
181+
182+
if (node.data.op === 'input') {
183+
// Seed from external inputs using node ID
184+
nodeInputs['out'] = inputMap[node.id] ?? node.data.value ?? 0;
185+
} else {
186+
// Gather values from incoming edges
187+
for (const edge of g.edges) {
188+
if (edge.targetId === node.id && edge.targetPort && edge.sourcePort) {
189+
const sourceVals = portValues.get(edge.sourceId);
190+
if (sourceVals && edge.sourcePort in sourceVals) {
191+
nodeInputs[edge.targetPort] = sourceVals[edge.sourcePort];
192+
}
193+
}
194+
}
195+
}
196+
197+
// Evaluate and store output port values
198+
const outputs = evaluate(node, nodeInputs);
199+
portValues.set(node.id, outputs);
200+
}
201+
202+
// Collect results from output nodes
203+
const results: Record<string, number> = {};
204+
for (const node of g.nodes) {
205+
if (node.data.op === 'output') {
206+
const vals = portValues.get(node.id);
207+
results[node.id] = vals?.['in'] ?? 0;
208+
}
209+
}
210+
211+
return results;
212+
}
213+
214+
// --- Demo ---
215+
216+
console.log('Flow graph: output = (input * 2) + 10');
217+
console.log(` initialNodeId: "${graph.initialNodeId}"\n`);
218+
219+
// Single number seeds graph.initialNodeId ("input" node)
220+
const test1 = run(graph, 42);
221+
console.log(`run(graph, 42) → ${JSON.stringify(test1)}`);
222+
// { output: 94 }
223+
224+
const test2 = run(graph, 0);
225+
console.log(`run(graph, 0) → ${JSON.stringify(test2)}`);
226+
// { output: 10 }
227+
228+
const test3 = run(graph, -5);
229+
console.log(`run(graph, -5) → ${JSON.stringify(test3)}`);
230+
// { output: 0 }
231+
232+
// Record form still works for multi-input graphs
233+
const test4 = run(graph, { input: 100 });
234+
console.log(`run(graph, { input: 100 }) → ${JSON.stringify(test4)}`);
235+
// { output: 210 }
236+
237+
console.log('\nPort inspection:');
238+
for (const node of graph.nodes) {
239+
const ports = getPorts(graph, node.id);
240+
if (ports.length > 0) {
241+
const portInfo = ports
242+
.map((p) => `${p.name}(${p.direction})`)
243+
.join(', ');
244+
console.log(` ${node.id}: [${portInfo}]`);
245+
}
246+
}
247+
248+
console.log('\nEdge routing:');
249+
for (const edge of graph.edges) {
250+
console.log(
251+
` ${edge.sourceId}:${edge.sourcePort}${edge.targetId}:${edge.targetPort}`,
252+
);
253+
}
254+
255+
// --- Verify with port queries ---
256+
257+
console.log('\nPort query: edges connected to add:a');
258+
const addAEdges = getEdgesByPort(graph, 'add', 'a');
259+
console.log(` ${addAEdges.map((e) => e.id).join(', ')}`);
260+
// e2

0 commit comments

Comments
 (0)