Skip to content

Commit bdc68a4

Browse files
Merge pull request #94 from festim-dev/fix-function-spawn
Improvements to Function node + foundation for extensions
2 parents 3b827eb + c59f759 commit bdc68a4

File tree

10 files changed

+221
-153
lines changed

10 files changed

+221
-153
lines changed

src/App.jsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import IntegratorNode from './IntegratorNode';
2424
import AdderNode from './AdderNode';
2525
import ScopeNode from './ScopeNode';
2626
import StepSourceNode from './StepSourceNode';
27-
import FunctionNode from './FunctionNode';
27+
import {createFunctionNode} from './FunctionNode';
2828
import DefaultNode from './DefaultNode';
2929
import { makeEdge } from './CustomEdge';
3030
import MultiplierNode from './MultiplierNode';
@@ -46,7 +46,8 @@ const nodeTypes = {
4646
adder: AdderNode,
4747
multiplier: MultiplierNode,
4848
scope: ScopeNode,
49-
function: FunctionNode,
49+
function: createFunctionNode(1, 1), // Default FunctionNode with 1 input and 1 output
50+
function2to2: createFunctionNode(2, 2), // FunctionNode with 2 inputs and 2 outputs
5051
rng: DefaultNode,
5152
pid: DefaultNode,
5253
splitter2: Splitter2Node,

src/FunctionNode.jsx

Lines changed: 121 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,126 @@ import React from 'react';
22
import { Handle } from '@xyflow/react';
33
import CustomHandle from './CustomHandle';
44

5-
export default function FunctionNode({ data }) {
6-
return (
7-
<div
8-
style={{
9-
width: 180,
10-
background: '#DDE6ED',
11-
color: 'black',
12-
borderRadius: 0,
13-
padding: 10,
14-
fontWeight: 'bold',
15-
position: 'relative',
16-
cursor: 'pointer',
17-
}}
18-
>
19-
<div style={{ marginBottom: 4 }}>{data.label}</div>
5+
// Factory function to create a FunctionNode component with specified inputs and outputs
6+
export function createFunctionNode(numInputs, numOutputs) {
7+
return function FunctionNode({ data }) {
8+
// Calculate dynamic width based on handle counts and content
9+
const minWidth = 180;
10+
const labelWidth = (data.label?.length || 8) * 8; // Rough character width estimation
11+
const dynamicWidth = Math.max(minWidth, labelWidth + 40);
12+
13+
// Calculate dynamic height based on maximum handle count
14+
const maxHandles = Math.max(numInputs, numOutputs);
15+
const minHeight = 60;
16+
const dynamicHeight = Math.max(minHeight, maxHandles * 25 + 30);
2017

21-
<CustomHandle type="target" position="left" style={{ background: '#555' }} connectionCount={1}/>
22-
<Handle type="source" position="right" style={{ background: '#555' }} />
23-
</div>
24-
);
18+
const handleStyle = { background: '#555' };
19+
20+
// Create input handles (targets)
21+
const inputHandles = [];
22+
for (let i = 0; i < numInputs; i++) {
23+
const handleId = `target-${i}`;
24+
const topPercentage = numInputs === 1 ? 50 : ((i + 1) / (numInputs + 1)) * 100;
25+
const connectionCount = data?.maxConnections?.[handleId] || 1;
26+
27+
inputHandles.push(
28+
<CustomHandle
29+
key={handleId}
30+
id={handleId}
31+
type="target"
32+
position="left"
33+
style={{ ...handleStyle, top: `${topPercentage}%` }}
34+
connectionCount={connectionCount}
35+
/>
36+
);
37+
38+
// Add label for multiple inputs
39+
if (numInputs > 1) {
40+
inputHandles.push(
41+
<div
42+
key={`${handleId}-label`}
43+
style={{
44+
position: 'absolute',
45+
left: '8px',
46+
top: `${topPercentage}%`,
47+
transform: 'translateY(-50%)',
48+
fontSize: '10px',
49+
fontWeight: 'normal',
50+
color: '#666',
51+
pointerEvents: 'none',
52+
}}
53+
>
54+
{i + 1}
55+
</div>
56+
);
57+
}
58+
}
59+
60+
// Create output handles (sources)
61+
const outputHandles = [];
62+
for (let i = 0; i < numOutputs; i++) {
63+
const handleId = `source-${i}`;
64+
const topPercentage = numOutputs === 1 ? 50 : ((i + 1) / (numOutputs + 1)) * 100;
65+
66+
outputHandles.push(
67+
<Handle
68+
key={handleId}
69+
id={handleId}
70+
type="source"
71+
position="right"
72+
style={{ ...handleStyle, top: `${topPercentage}%` }}
73+
/>
74+
);
75+
76+
// Add label for multiple outputs
77+
if (numOutputs > 1) {
78+
outputHandles.push(
79+
<div
80+
key={`${handleId}-label`}
81+
style={{
82+
position: 'absolute',
83+
right: '8px',
84+
top: `${topPercentage}%`,
85+
transform: 'translateY(-50%)',
86+
fontSize: '10px',
87+
fontWeight: 'normal',
88+
color: '#666',
89+
pointerEvents: 'none',
90+
}}
91+
>
92+
{i + 1}
93+
</div>
94+
);
95+
}
96+
}
97+
98+
return (
99+
<div
100+
style={{
101+
width: dynamicWidth,
102+
height: dynamicHeight,
103+
background: '#DDE6ED',
104+
color: 'black',
105+
borderRadius: 0,
106+
padding: 10,
107+
fontWeight: 'bold',
108+
position: 'relative',
109+
cursor: 'pointer',
110+
display: 'flex',
111+
alignItems: 'center',
112+
justifyContent: 'center',
113+
}}
114+
>
115+
<div style={{ textAlign: 'center', wordWrap: 'break-word', maxWidth: '100%' }}>
116+
{data.label}
117+
</div>
118+
119+
{inputHandles}
120+
{outputHandles}
121+
</div>
122+
);
123+
};
25124
}
125+
126+
// Default FunctionNode with 1 input and 1 output
127+
export default createFunctionNode(1, 1);

src/backend.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,10 @@ def convert_to_python():
147147
except Exception as e:
148148
return jsonify({"success": False, "error": f"Server error: {str(e)}"}), 500
149149

150+
150151
# Helper function to extract CSV payload from scopes
151152
def make_csv_payload(scopes):
152-
csv_payload = {
153-
"time": [],
154-
"series": {}
155-
}
153+
csv_payload = {"time": [], "series": {}}
156154

157155
max_len = 0
158156
for scope in scopes:
@@ -165,6 +163,7 @@ def make_csv_payload(scopes):
165163

166164
return csv_payload
167165

166+
168167
# Function to convert graph to pathsim and run simulation
169168
@app.route("/run-pathsim", methods=["POST"])
170169
def run_pathsim():
@@ -225,13 +224,14 @@ def run_pathsim():
225224
# Convert plot to JSON
226225
plot_data = plotly_json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder)
227226

228-
return jsonify({
227+
return jsonify(
228+
{
229229
"success": True,
230230
"plot": plot_data,
231231
"csv_data": csv_payload,
232-
"message": "Pathsim simulation completed successfully"
233-
})
234-
232+
"message": "Pathsim simulation completed successfully",
233+
}
234+
)
235235

236236
except Exception as e:
237237
return jsonify({"success": False, "error": f"Server error: {str(e)}"}), 500

src/convert_to_python.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33
from inspect import signature
44

55
from pathsim.blocks import Scope
6-
from .custom_pathsim_blocks import Process, Splitter, Bubbler, FestimWall
6+
from .custom_pathsim_blocks import (
7+
Process,
8+
Splitter,
9+
Bubbler,
10+
FestimWall,
11+
Function1to1,
12+
Function2to2,
13+
)
714
from .pathsim_utils import (
815
map_str_to_object,
916
make_blocks,
@@ -167,6 +174,19 @@ def make_edge_data(data: dict) -> list[dict]:
167174
raise ValueError(
168175
f"Invalid source handle '{edge['sourceHandle']}' for {edge}."
169176
)
177+
elif isinstance(block, Function1to1):
178+
# Function1to1 has only one output port
179+
output_index = 0
180+
elif isinstance(block, Function2to2):
181+
# Function2to2 has two output ports
182+
if edge["sourceHandle"] == "output-0":
183+
output_index = 0
184+
elif edge["sourceHandle"] == "output-1":
185+
output_index = 1
186+
else:
187+
raise ValueError(
188+
f"Invalid source handle '{edge['sourceHandle']}' for {edge}."
189+
)
170190
else:
171191
# make sure that the source block has only one output port (ie. that sourceHandle is None)
172192
assert edge["sourceHandle"] is None, (
@@ -195,6 +215,19 @@ def make_edge_data(data: dict) -> list[dict]:
195215
raise ValueError(
196216
f"Invalid target handle '{edge['targetHandle']}' for {edge}."
197217
)
218+
elif isinstance(target_block, Function1to1):
219+
# Function1to1 has only one input port
220+
input_index = 0
221+
elif isinstance(target_block, Function2to2):
222+
# Function2to2 has two input ports
223+
if edge["targetHandle"] == "input-0":
224+
input_index = 0
225+
elif edge["targetHandle"] == "input-1":
226+
input_index = 1
227+
else:
228+
raise ValueError(
229+
f"Invalid target handle '{edge['targetHandle']}' for {edge}."
230+
)
198231
else:
199232
# make sure that the target block has only one input port (ie. that targetHandle is None)
200233
assert edge["targetHandle"] is None, (

src/custom_pathsim_blocks.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,28 @@ def create_reset_events(self):
9494
]
9595

9696

97+
class Function1to1(pathsim.blocks.Function):
98+
"""Function block with 1 input and 1 output."""
99+
100+
def __init__(self, expression="lambda x: 1*x"):
101+
if isinstance(expression, str):
102+
func = eval(expression)
103+
else:
104+
func = expression
105+
super().__init__(func=func)
106+
107+
108+
class Function2to2(pathsim.blocks.Function):
109+
"""Function block with 2 inputs and 2 outputs."""
110+
111+
def __init__(self, expression="lambda x, y:1*x, 1*y"):
112+
if isinstance(expression, str):
113+
func = eval(expression)
114+
else:
115+
func = expression
116+
super().__init__(func=func)
117+
118+
97119
# BUBBLER SYSTEM
98120

99121

0 commit comments

Comments
 (0)