Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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: 3 additions & 2 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
Background,
useNodesState,
useEdgesState,
addEdge,

Check failure on line 9 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

'addEdge' is defined but never used. Allowed unused vars must match /^[A-Z_]/u

Check failure on line 9 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

'addEdge' is defined but never used. Allowed unused vars must match /^[A-Z_]/u
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import './App.css';
Expand All @@ -24,7 +24,7 @@
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';
Expand All @@ -46,7 +46,8 @@
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,
Expand Down Expand Up @@ -803,7 +804,7 @@
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [selectedEdge, selectedNode, copiedNode, duplicateNode, setCopyFeedback]);

Check warning on line 807 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

React Hook useEffect has missing dependencies: 'deleteSelectedEdge' and 'deleteSelectedNode'. Either include them or remove the dependency array

Check warning on line 807 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

React Hook useEffect has missing dependencies: 'deleteSelectedEdge' and 'deleteSelectedNode'. Either include them or remove the dependency array

return (
<div style={{ width: '100vw', height: '100vh', position: 'relative' }}>
Expand Down
140 changes: 121 additions & 19 deletions src/FunctionNode.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,126 @@
import { Handle } from '@xyflow/react';
import CustomHandle from './CustomHandle';

export default function FunctionNode({ data }) {
return (
<div
style={{
width: 180,
background: '#DDE6ED',
color: 'black',
borderRadius: 0,
padding: 10,
fontWeight: 'bold',
position: 'relative',
cursor: 'pointer',
}}
>
<div style={{ marginBottom: 4 }}>{data.label}</div>
// 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);

<CustomHandle type="target" position="left" style={{ background: '#555' }} connectionCount={1}/>
<Handle type="source" position="right" style={{ background: '#555' }} />
</div>
);
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(
<CustomHandle
key={handleId}
id={handleId}
type="target"
position="left"
style={{ ...handleStyle, top: `${topPercentage}%` }}
connectionCount={connectionCount}
/>
);

// Add label for multiple inputs
if (numInputs > 1) {
inputHandles.push(
<div
key={`${handleId}-label`}
style={{
position: 'absolute',
left: '8px',
top: `${topPercentage}%`,
transform: 'translateY(-50%)',
fontSize: '10px',
fontWeight: 'normal',
color: '#666',
pointerEvents: 'none',
}}
>
{i + 1}
</div>
);
}
}

// 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(
<Handle
key={handleId}
id={handleId}
type="source"
position="right"
style={{ ...handleStyle, top: `${topPercentage}%` }}
/>
);

// Add label for multiple outputs
if (numOutputs > 1) {
outputHandles.push(
<div
key={`${handleId}-label`}
style={{
position: 'absolute',
right: '8px',
top: `${topPercentage}%`,
transform: 'translateY(-50%)',
fontSize: '10px',
fontWeight: 'normal',
color: '#666',
pointerEvents: 'none',
}}
>
{i + 1}
</div>
);
}
}

return (
<div
style={{
width: dynamicWidth,
height: dynamicHeight,
background: '#DDE6ED',
color: 'black',
borderRadius: 0,
padding: 10,
fontWeight: 'bold',
position: 'relative',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<div style={{ textAlign: 'center', wordWrap: 'break-word', maxWidth: '100%' }}>
{data.label}
</div>

{inputHandles}
{outputHandles}
</div>
);
};
}

// Default FunctionNode with 1 input and 1 output
export default createFunctionNode(1, 1);

Check warning on line 127 in src/FunctionNode.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

Fast refresh can't handle anonymous components. Add a name to your export

Check warning on line 127 in src/FunctionNode.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

Fast refresh can't handle anonymous components. Add a name to your export
16 changes: 8 additions & 8 deletions src/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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():
Expand Down Expand Up @@ -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
Expand Down
35 changes: 34 additions & 1 deletion src/convert_to_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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, (
Expand Down Expand Up @@ -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, (
Expand Down
22 changes: 22 additions & 0 deletions src/custom_pathsim_blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
Loading
Loading