diff --git a/src/App.jsx b/src/App.jsx
index 92dcbb98..fd6630e9 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -24,7 +24,7 @@ import IntegratorNode from './IntegratorNode';
import AdderNode from './AdderNode';
import ScopeNode from './ScopeNode';
import StepSourceNode from './StepSourceNode';
-import FunctionNode from './FunctionNode';
+import {createFunctionNode} from './FunctionNode';
import DefaultNode from './DefaultNode';
import { makeEdge } from './CustomEdge';
import MultiplierNode from './MultiplierNode';
@@ -46,7 +46,8 @@ const nodeTypes = {
adder: AdderNode,
multiplier: MultiplierNode,
scope: ScopeNode,
- function: FunctionNode,
+ function: createFunctionNode(1, 1), // Default FunctionNode with 1 input and 1 output
+ function2to2: createFunctionNode(2, 2), // FunctionNode with 2 inputs and 2 outputs
rng: DefaultNode,
pid: DefaultNode,
splitter2: Splitter2Node,
diff --git a/src/FunctionNode.jsx b/src/FunctionNode.jsx
index b26ede38..8ae0b3ee 100644
--- a/src/FunctionNode.jsx
+++ b/src/FunctionNode.jsx
@@ -2,24 +2,126 @@ import React from 'react';
import { Handle } from '@xyflow/react';
import CustomHandle from './CustomHandle';
-export default function FunctionNode({ data }) {
- return (
-
-
{data.label}
+// Factory function to create a FunctionNode component with specified inputs and outputs
+export function createFunctionNode(numInputs, numOutputs) {
+ return function FunctionNode({ data }) {
+ // Calculate dynamic width based on handle counts and content
+ const minWidth = 180;
+ const labelWidth = (data.label?.length || 8) * 8; // Rough character width estimation
+ const dynamicWidth = Math.max(minWidth, labelWidth + 40);
+
+ // Calculate dynamic height based on maximum handle count
+ const maxHandles = Math.max(numInputs, numOutputs);
+ const minHeight = 60;
+ const dynamicHeight = Math.max(minHeight, maxHandles * 25 + 30);
-
-
-
- );
+ const handleStyle = { background: '#555' };
+
+ // Create input handles (targets)
+ const inputHandles = [];
+ for (let i = 0; i < numInputs; i++) {
+ const handleId = `target-${i}`;
+ const topPercentage = numInputs === 1 ? 50 : ((i + 1) / (numInputs + 1)) * 100;
+ const connectionCount = data?.maxConnections?.[handleId] || 1;
+
+ inputHandles.push(
+
+ );
+
+ // Add label for multiple inputs
+ if (numInputs > 1) {
+ inputHandles.push(
+
+ {i + 1}
+
+ );
+ }
+ }
+
+ // Create output handles (sources)
+ const outputHandles = [];
+ for (let i = 0; i < numOutputs; i++) {
+ const handleId = `source-${i}`;
+ const topPercentage = numOutputs === 1 ? 50 : ((i + 1) / (numOutputs + 1)) * 100;
+
+ outputHandles.push(
+
+ );
+
+ // Add label for multiple outputs
+ if (numOutputs > 1) {
+ outputHandles.push(
+
+ {i + 1}
+
+ );
+ }
+ }
+
+ return (
+
+
+ {data.label}
+
+
+ {inputHandles}
+ {outputHandles}
+
+ );
+ };
}
+
+// Default FunctionNode with 1 input and 1 output
+export default createFunctionNode(1, 1);
diff --git a/src/backend.py b/src/backend.py
index a620d903..25f1e2c1 100644
--- a/src/backend.py
+++ b/src/backend.py
@@ -147,12 +147,10 @@ def convert_to_python():
except Exception as e:
return jsonify({"success": False, "error": f"Server error: {str(e)}"}), 500
+
# Helper function to extract CSV payload from scopes
def make_csv_payload(scopes):
- csv_payload = {
- "time": [],
- "series": {}
- }
+ csv_payload = {"time": [], "series": {}}
max_len = 0
for scope in scopes:
@@ -165,6 +163,7 @@ def make_csv_payload(scopes):
return csv_payload
+
# Function to convert graph to pathsim and run simulation
@app.route("/run-pathsim", methods=["POST"])
def run_pathsim():
@@ -225,13 +224,14 @@ def run_pathsim():
# Convert plot to JSON
plot_data = plotly_json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder)
- return jsonify({
+ return jsonify(
+ {
"success": True,
"plot": plot_data,
"csv_data": csv_payload,
- "message": "Pathsim simulation completed successfully"
- })
-
+ "message": "Pathsim simulation completed successfully",
+ }
+ )
except Exception as e:
return jsonify({"success": False, "error": f"Server error: {str(e)}"}), 500
diff --git a/src/convert_to_python.py b/src/convert_to_python.py
index a8685f48..e30dcf48 100644
--- a/src/convert_to_python.py
+++ b/src/convert_to_python.py
@@ -3,7 +3,14 @@
from inspect import signature
from pathsim.blocks import Scope
-from .custom_pathsim_blocks import Process, Splitter, Bubbler, FestimWall
+from .custom_pathsim_blocks import (
+ Process,
+ Splitter,
+ Bubbler,
+ FestimWall,
+ Function1to1,
+ Function2to2,
+)
from .pathsim_utils import (
map_str_to_object,
make_blocks,
@@ -167,6 +174,19 @@ def make_edge_data(data: dict) -> list[dict]:
raise ValueError(
f"Invalid source handle '{edge['sourceHandle']}' for {edge}."
)
+ elif isinstance(block, Function1to1):
+ # Function1to1 has only one output port
+ output_index = 0
+ elif isinstance(block, Function2to2):
+ # Function2to2 has two output ports
+ if edge["sourceHandle"] == "output-0":
+ output_index = 0
+ elif edge["sourceHandle"] == "output-1":
+ output_index = 1
+ else:
+ raise ValueError(
+ f"Invalid source handle '{edge['sourceHandle']}' for {edge}."
+ )
else:
# make sure that the source block has only one output port (ie. that sourceHandle is None)
assert edge["sourceHandle"] is None, (
@@ -195,6 +215,19 @@ def make_edge_data(data: dict) -> list[dict]:
raise ValueError(
f"Invalid target handle '{edge['targetHandle']}' for {edge}."
)
+ elif isinstance(target_block, Function1to1):
+ # Function1to1 has only one input port
+ input_index = 0
+ elif isinstance(target_block, Function2to2):
+ # Function2to2 has two input ports
+ if edge["targetHandle"] == "input-0":
+ input_index = 0
+ elif edge["targetHandle"] == "input-1":
+ input_index = 1
+ else:
+ raise ValueError(
+ f"Invalid target handle '{edge['targetHandle']}' for {edge}."
+ )
else:
# make sure that the target block has only one input port (ie. that targetHandle is None)
assert edge["targetHandle"] is None, (
diff --git a/src/custom_pathsim_blocks.py b/src/custom_pathsim_blocks.py
index f9a51f3c..7f4812ee 100644
--- a/src/custom_pathsim_blocks.py
+++ b/src/custom_pathsim_blocks.py
@@ -94,6 +94,28 @@ def create_reset_events(self):
]
+class Function1to1(pathsim.blocks.Function):
+ """Function block with 1 input and 1 output."""
+
+ def __init__(self, expression="lambda x: 1*x"):
+ if isinstance(expression, str):
+ func = eval(expression)
+ else:
+ func = expression
+ super().__init__(func=func)
+
+
+class Function2to2(pathsim.blocks.Function):
+ """Function block with 2 inputs and 2 outputs."""
+
+ def __init__(self, expression="lambda x, y:1*x, 1*y"):
+ if isinstance(expression, str):
+ func = eval(expression)
+ else:
+ func = expression
+ super().__init__(func=func)
+
+
# BUBBLER SYSTEM
diff --git a/src/pathsim_utils.py b/src/pathsim_utils.py
index 0eebbc74..d20371cd 100644
--- a/src/pathsim_utils.py
+++ b/src/pathsim_utils.py
@@ -12,7 +12,7 @@
Amplifier,
Adder,
Multiplier,
- Function,
+ # Function,
Delay,
RNG,
PID,
@@ -27,6 +27,8 @@
Bubbler,
FestimWall,
Integrator,
+ Function1to1,
+ Function2to2,
)
from flask import jsonify
import inspect
@@ -53,7 +55,8 @@
"rng": RNG,
"pid": PID,
"integrator": Integrator,
- "function": Function,
+ "function": Function1to1,
+ "function2to2": Function2to2,
"delay": Delay,
"bubbler": Bubbler,
"wall": FestimWall,
@@ -92,51 +95,6 @@ def create_integrator(
return block, events
-def create_function(node: dict, eval_namespace: dict = None) -> Block:
- if eval_namespace is None:
- eval_namespace = globals()
-
- # Convert the expression string to a lambda function
- expression = node["data"].get("expression", "x")
-
- # Create a safe lambda function from the expression
- # The expression should use 'x' as the variable
- try:
- # Create a lambda function from the expression string
- # We'll allow common mathematical operations and numpy functions
-
- # Safe namespace for eval - merge with global variables
- safe_namespace = {
- "x": 0, # placeholder
- "np": np,
- "math": math,
- "sin": np.sin,
- "cos": np.cos,
- "tan": np.tan,
- "exp": np.exp,
- "log": np.log,
- "sqrt": np.sqrt,
- "abs": abs,
- "pow": pow,
- "pi": np.pi,
- "e": np.e,
- **eval_namespace, # Include global variables
- }
-
- # Test the expression first to ensure it's valid
- eval(expression.replace("x", "1"), safe_namespace)
-
- # Create the actual function
- def func(x):
- return eval(expression, {**safe_namespace, "x": x})
-
- except Exception as e:
- raise ValueError(f"Invalid function expression: {expression}. Error: {e}")
-
- block = Function(func=func)
- return block
-
-
def create_bubbler(node: dict) -> Bubbler:
"""
Create a Bubbler block based on the node data.
@@ -336,15 +294,8 @@ def make_blocks(
if block_type == "integrator":
block, event_int = create_integrator(node, eval_namespace)
events.extend(event_int)
- elif block_type == "function":
- block = create_function(node, eval_namespace)
elif block_type == "scope":
block = create_scope(node, edges, nodes)
- elif block_type == "stepsource":
- block = StepSource(
- amplitude=eval(node["data"]["amplitude"], eval_namespace),
- tau=eval(node["data"]["delay"], eval_namespace),
- )
elif block_type == "splitter2":
block = Splitter2(
f1=eval(node["data"]["f1"], eval_namespace),
@@ -430,6 +381,19 @@ def make_connections(nodes, edges, blocks) -> list[Connection]:
raise ValueError(
f"Invalid source handle '{edge['sourceHandle']}' for {edge}."
)
+ elif isinstance(block, Function1to1):
+ # Function1to1 has only one output port
+ output_index = 0
+ elif isinstance(block, Function2to2):
+ # Function2to2 has two output ports
+ if edge["sourceHandle"] == "output-0":
+ output_index = 0
+ elif edge["sourceHandle"] == "output-1":
+ output_index = 1
+ else:
+ raise ValueError(
+ f"Invalid source handle '{edge['sourceHandle']}' for {edge}."
+ )
else:
# make sure that the source block has only one output port (ie. that sourceHandle is None)
assert edge["sourceHandle"] is None, (
@@ -458,6 +422,19 @@ def make_connections(nodes, edges, blocks) -> list[Connection]:
raise ValueError(
f"Invalid target handle '{edge['targetHandle']}' for {edge}."
)
+ elif isinstance(target_block, Function1to1):
+ # Function1to1 has only one input port
+ input_index = 0
+ elif isinstance(target_block, Function2to2):
+ # Function2to2 has two input ports
+ if edge["targetHandle"] == "input-0":
+ input_index = 0
+ elif edge["targetHandle"] == "input-1":
+ input_index = 1
+ else:
+ raise ValueError(
+ f"Invalid target handle '{edge['targetHandle']}' for {edge}."
+ )
else:
# make sure that the target block has only one input port (ie. that targetHandle is None)
assert edge["targetHandle"] is None, (
diff --git a/src/templates/block_macros.py b/src/templates/block_macros.py
index 7137ce5c..ce5c3164 100644
--- a/src/templates/block_macros.py
+++ b/src/templates/block_macros.py
@@ -32,25 +32,6 @@
{%- endmacro -%}
-
-{% macro create_function_block(node) -%}
-
-def func(x):
- return {{ node["data"]["expression"] }}
-
-{{ node["var_name"] }} = pathsim.blocks.Function(func=func)
-
-{%- endmacro -%}
-
-{% macro create_stepsource(node) -%}
-{{ node["var_name"] }} = pathsim.blocks.StepSource(
- amplitude={{ node["data"]["amplitude"] }},
- tau={{ node["data"]["delay"] }},
-)
-{%- endmacro -%}
-
-
-
{% macro create_scope_block(node) -%}
{{ node["var_name"] }} = pathsim.blocks.Scope(
labels={{ node["labels"] }}
diff --git a/src/templates/template_with_macros.py b/src/templates/template_with_macros.py
index 9fb28e53..2e7344c0 100644
--- a/src/templates/template_with_macros.py
+++ b/src/templates/template_with_macros.py
@@ -4,7 +4,7 @@
import matplotlib.pyplot as plt
import src
{# Import macros #}
-{% from 'block_macros.py' import create_block, create_source_block, create_integrator_block, create_function_block, create_scope_block, create_stepsource, create_bubbler_block, create_connections -%}
+{% from 'block_macros.py' import create_block, create_source_block, create_integrator_block, create_scope_block, create_bubbler_block, create_connections -%}
# Create global variables
{% for var in globalVariables -%}
@@ -16,10 +16,6 @@
{% for node in nodes -%}
{%- if node["type"] == "integrator" -%}
{{ create_integrator_block(node) }}
-{%- elif node["type"] == "stepsource" -%}
-{{ create_stepsource(node) }}
-{%- elif node["type"] == "function" -%}
-{{ create_function_block(node) }}
{%- elif node["type"] == "scope" -%}
{{ create_scope_block(node) }}
{%- elif node["type"] == "bubbler" -%}
diff --git a/test/test_backend.py b/test/test_backend.py
index 0937dcda..838837e1 100644
--- a/test/test_backend.py
+++ b/test/test_backend.py
@@ -1,7 +1,6 @@
from src.pathsim_utils import (
create_integrator,
auto_block_construction,
- create_function,
create_bubbler,
create_scope,
)
@@ -190,17 +189,6 @@ def test_auto_block_construction_with_var(node_factory, block_type, expected_cla
assert isinstance(block, expected_class)
-def test_create_function():
- node = {
- "data": {"expression": "3*x**2 + b", "label": "Function"},
- "id": "10",
- "type": "function",
- }
- block = create_function(node, eval_namespace={"b": 2.5})
- assert isinstance(block, pathsim.blocks.Function)
- assert block.func(2) == 3 * 2**2 + 2.5
-
-
def test_create_bubbler():
node = {
"id": "6",
diff --git a/test/test_convert_python.py b/test/test_convert_python.py
index fc87013e..27a01be4 100644
--- a/test/test_convert_python.py
+++ b/test/test_convert_python.py
@@ -29,7 +29,7 @@
"type": "function",
"data": {
"label": "func_block",
- "expression": "x * 2 + 1",
+ "expression": "lambda x: x * 2 + 1",
},
},
{
@@ -129,38 +129,6 @@ def test_nested_templates(data):
assert False
-def test_stepsource_delay_converted_to_tau():
- "Test that the delay parameter in a stepsource node is converted to tau in the generated code."
- sample_data = {
- "nodes": [
- {
- "id": "1",
- "type": "stepsource",
- "data": {
- "label": "input_signal",
- "delay": "3.0",
- "amplitude": "2.0",
- },
- },
- ],
- "edges": [],
- "solverParams": {
- "Solver": "SSPRK22",
- "dt": "0.01",
- "dt_max": "1.0",
- "dt_min": "1e-6",
- "extra_params": "{}",
- "iterations_max": "100",
- "log": "true",
- "simulation_duration": "duration",
- "tolerance_fpi": "1e-6",
- },
- "globalVariables": [],
- }
- code = convert_graph_to_python(sample_data)
- assert "tau=3.0" in code
-
-
def test_bubbler_has_reset_times():
"""Test that the bubbler node has reset times in the generated code."""
sample_data = {