From 423f9a816bad44afdd36ae39b198f4fad0c6bb26 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Fri, 1 Aug 2025 20:12:24 -0400 Subject: [PATCH 1/6] formatting --- src/backend.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 From 503074fbc174cd82b43de33b198e54d89fef4c32 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Fri, 1 Aug 2025 20:51:45 -0400 Subject: [PATCH 2/6] function factory style --- src/App.jsx | 5 +- src/FunctionNode.jsx | 140 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 124 insertions(+), 21 deletions(-) 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); From 877a3d8f0ba35921c65197c44033a2f30ab58924 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Fri, 1 Aug 2025 21:32:41 -0400 Subject: [PATCH 3/6] overloaded Function1to1 and Function2to2 --- src/custom_pathsim_blocks.py | 14 +++++++ src/pathsim_utils.py | 80 ++++++++++++++---------------------- 2 files changed, 45 insertions(+), 49 deletions(-) diff --git a/src/custom_pathsim_blocks.py b/src/custom_pathsim_blocks.py index f9a51f3c..46e8c32b 100644 --- a/src/custom_pathsim_blocks.py +++ b/src/custom_pathsim_blocks.py @@ -94,6 +94,20 @@ 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"): + super().__init__(func=eval(expression)) + + +class Function2to2(pathsim.blocks.Function): + """Function block with 2 inputs and 2 outputs.""" + + def __init__(self, expression="lambda x, y:1*x, 1*y"): + super().__init__(func=eval(expression)) + + # BUBBLER SYSTEM diff --git a/src/pathsim_utils.py b/src/pathsim_utils.py index 0eebbc74..61bcc674 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,8 +294,6 @@ 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": @@ -430,6 +386,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 +427,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, ( From 578c3b2c4ca483fb73272a5dd21ba893c803b57f Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Fri, 1 Aug 2025 21:42:05 -0400 Subject: [PATCH 4/6] simplified utils and conversion for function --- src/convert_to_python.py | 35 ++++++++++++++++++++++++++- src/pathsim_utils.py | 5 ---- src/templates/block_macros.py | 19 --------------- src/templates/template_with_macros.py | 6 +---- 4 files changed, 35 insertions(+), 30 deletions(-) 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/pathsim_utils.py b/src/pathsim_utils.py index 61bcc674..d20371cd 100644 --- a/src/pathsim_utils.py +++ b/src/pathsim_utils.py @@ -296,11 +296,6 @@ def make_blocks( events.extend(event_int) 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), 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" -%} From eab296b9bf1506ac6ee087667bf119cb52260b0a Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Fri, 1 Aug 2025 21:47:26 -0400 Subject: [PATCH 5/6] tests pass --- src/custom_pathsim_blocks.py | 12 ++++++++++-- test/test_convert_python.py | 34 +--------------------------------- 2 files changed, 11 insertions(+), 35 deletions(-) diff --git a/src/custom_pathsim_blocks.py b/src/custom_pathsim_blocks.py index 46e8c32b..7f4812ee 100644 --- a/src/custom_pathsim_blocks.py +++ b/src/custom_pathsim_blocks.py @@ -98,14 +98,22 @@ class Function1to1(pathsim.blocks.Function): """Function block with 1 input and 1 output.""" def __init__(self, expression="lambda x: 1*x"): - super().__init__(func=eval(expression)) + 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"): - super().__init__(func=eval(expression)) + if isinstance(expression, str): + func = eval(expression) + else: + func = expression + super().__init__(func=func) # BUBBLER SYSTEM 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 = { From c59f759822402746dfa1d9551b86983599cfedbf Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Fri, 1 Aug 2025 22:00:24 -0400 Subject: [PATCH 6/6] removed obsolete test --- test/test_backend.py | 12 ------------ 1 file changed, 12 deletions(-) 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",