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
585 changes: 585 additions & 0 deletions example_graphs/stick_slip.json

Large diffs are not rendered by default.

44 changes: 22 additions & 22 deletions src/components/EventsTab.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
...eventDefaults[initialEventType]
};
});

// State to track if we're editing an existing event
const [editingEventId, setEditingEventId] = useState(null);

Expand Down Expand Up @@ -79,21 +79,21 @@
const addEvent = () => {
if (currentEvent.name) {
// Validate required fields based on event type

// For Schedule, func_act is required
if (['Schedule', 'ScheduleList'].includes(currentEvent.type) && !currentEvent.func_act) {
alert('func_act is required for Schedule events');
return;
}

// For other event types, both func_evt and func_act are typically required
if (!['Schedule', 'ScheduleList'].includes(currentEvent.type) && (!currentEvent.func_evt || !currentEvent.func_act)) {
alert('Both func_evt and func_act are required for this event type');
return;
}

setEvents(prev => [...prev, { ...currentEvent, id: Date.now() }]);

// Reset to defaults for current type
const resetDefaults = eventDefaults[currentEvent.type] || {};
setCurrentEvent({
Expand All @@ -111,23 +111,23 @@

const saveEditedEvent = () => {
if (currentEvent.name) {

// For Schedule, func_act is required
if (currentEvent.type === 'Schedule' && !currentEvent.func_act) {
alert('func_act is required for Schedule events');
return;
}

// For other event types, both func_evt and func_act are typically required
if (currentEvent.type !== 'Schedule' && (!currentEvent.func_evt || !currentEvent.func_act)) {
alert('Both func_evt and func_act are required for this event type');
return;
}

setEvents(prev => prev.map(event =>
setEvents(prev => prev.map(event =>
event.id === editingEventId ? { ...currentEvent } : event
));

// Reset form and exit edit mode
cancelEdit();
}
Expand Down Expand Up @@ -179,7 +179,7 @@
<h2 style={{ color: '#ffffff', marginBottom: '20px' }}>
{editingEventId ? 'Edit Event' : 'Add New Event'}
</h2>

<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div style={{ width: '100%', maxWidth: '400px' }}>
<label style={{ color: '#ffffff', display: 'block', marginBottom: '8px' }}>
Expand All @@ -203,7 +203,7 @@
</div>

<div style={{ width: '100%', maxWidth: '400px' }}>
<label style={{ color: '#ffffff', display: 'block', marginBottom: '8px'}}>
<label style={{ color: '#ffffff', display: 'block', marginBottom: '8px' }}>
Event Type:
</label>
<select
Expand Down Expand Up @@ -241,15 +241,15 @@
const defaultValue = typeDefaults[key];
const placeholder = defaultValue !== undefined && defaultValue !== null ?
String(defaultValue) : '';

// Check if this is a function parameter (contains 'func' in the name)
const isFunctionParam = key.toLowerCase().includes('func');

return (
<div key={key} style={{ width: '100%', maxWidth: isFunctionParam ? '600px' : '400px' }}>
<label style={{
color: '#ffffff',
display: 'block',
<label style={{
color: '#ffffff',
display: 'block',
marginBottom: '8px',
fontSize: '14px',
fontWeight: '500',
Expand Down Expand Up @@ -354,7 +354,7 @@
{/* Events List */}
<div style={{ maxWidth: '800px', margin: '0 auto' }}>
<h2 style={{ color: '#ffffff', marginBottom: '20px', textAlign: 'center' }}>Defined Events ({events.length})</h2>

{events.length === 0 ? (
<div style={{
backgroundColor: '#2a2a3f',
Expand All @@ -368,7 +368,7 @@
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{events.map((event, index) => (

Check failure on line 371 in src/components/EventsTab.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

'index' is defined but never used

Check failure on line 371 in src/components/EventsTab.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

'index' is defined but never used
<div
key={event.id}
style={{
Expand All @@ -385,9 +385,9 @@
{event.name} ({event.type})
</h3>
{editingEventId === event.id && (
<span style={{
color: '#007bff',
fontSize: '12px',
<span style={{
color: '#007bff',
fontSize: '12px',
fontStyle: 'italic',
marginTop: '4px',
display: 'block'
Expand Down Expand Up @@ -434,12 +434,12 @@
.filter(([key]) => key !== 'id' && key !== 'name' && key !== 'type')
.map(([key, value]) => {
const isFunctionParam = key.toLowerCase().includes('func');

return (
<div key={key}>
<h4 style={{
color: '#ccc',
margin: '0 0 8px 0',
<h4 style={{
color: '#ccc',
margin: '0 0 8px 0',
fontSize: '14px',
textTransform: 'capitalize'
}}>
Expand Down
54 changes: 54 additions & 0 deletions src/components/nodes/SwitchNode.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from 'react';
import { Handle } from '@xyflow/react';

export default function SwitchNode({ data }) {
return (
<div
style={{
width: 180,
background: data.nodeColor || '#DDE6ED',
color: 'black',
borderRadius: 0,
padding: 10,
fontWeight: 'bold',
position: 'relative',
cursor: 'pointer',
}}
>
<div style={{ marginBottom: 4, marginLeft: 20}}>{data.label}</div>

<Handle type="target" id="target-0" position="left" style={{ background: '#555', top: "33%" }} />
<div
style={{
position: 'absolute',
left: '8px',
top: `33%`,
transform: 'translateY(-50%)',
fontSize: '10px',
fontWeight: 'normal',
color: '#666',
pointerEvents: 'none',
}}
>
0
</div>
<Handle type="target" id="target-1" position="left" style={{ background: '#555', top: "66%" }} />

<div
style={{
position: 'absolute',
left: '8px',
top: `66%`,
transform: 'translateY(-50%)',
fontSize: '10px',
fontWeight: 'normal',
color: '#666',
pointerEvents: 'none',
}}
>
1
</div>
<Handle type="source" position="right" style={{ background: '#555' }} />
</div>
);
}
11 changes: 9 additions & 2 deletions src/nodeConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
import AdderNode from './components/nodes/AdderNode';
import ScopeNode from './components/nodes/ScopeNode';
import StepSourceNode from './components/nodes/StepSourceNode';
import { createFunctionNode } from './components/nodes/FunctionNode';

Check failure on line 10 in src/nodeConfig.js

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

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

Check failure on line 10 in src/nodeConfig.js

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

'createFunctionNode' is defined but never used. Allowed unused vars must match /^[A-Z_]/u
import DefaultNode from './components/nodes/DefaultNode';
import MultiplierNode from './components/nodes/MultiplierNode';
import { Splitter2Node, Splitter3Node } from './components/nodes/Splitters';
import BubblerNode from './components/nodes/BubblerNode';
import WallNode from './components/nodes/WallNode';
import { DynamicHandleNode } from './components/nodes/DynamicHandleNode';
import SwitchNode from './components/nodes/SwitchNode';

// Node types mapping
export const nodeTypes = {
Expand Down Expand Up @@ -59,6 +60,8 @@
butterworthbandstop: DefaultNode,
fir: DefaultNode,
ode: DynamicHandleNode,
interface: DynamicHandleNode,
switch: SwitchNode,
};

export const nodeMathTypes = {
Expand Down Expand Up @@ -87,7 +90,7 @@
}
});

export const nodeDynamicHandles = ['ode', 'function'];
export const nodeDynamicHandles = ['ode', 'function', 'interface'];

// Node categories for better organization
export const nodeCategories = {
Expand Down Expand Up @@ -116,7 +119,7 @@
description: 'Fuel cycle specific nodes'
},
'Others': {
nodes: ['samplehold', 'comparator'],
nodes: ['samplehold', 'comparator', 'switch', 'interface'],
description: 'Miscellaneous nodes'
},
'Output': {
Expand Down Expand Up @@ -184,6 +187,10 @@
'butterworthbandpass': 'Butterworth Band-Pass Filter',
'butterworthbandstop': 'Butterworth Band-Stop Filter',
'fir': 'FIR Filter',
'switch': 'Switch',
'samplehold': 'Sample Hold',
'comparator': 'Comparator',
'interface': 'Interface',
};

return displayNames[nodeType] || nodeType.charAt(0).toUpperCase() + nodeType.slice(1);
Expand Down
38 changes: 27 additions & 11 deletions src/python/convert_to_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,18 +160,34 @@ def make_events_data(data: dict) -> list[dict]:
event["expected_arguments"] = signature(event_class).parameters

if "func_evt" in event:
# replace the name of the function by something unique
func_evt = event["func_evt"]
func_evt = func_evt.replace("def func_evt", f"def {event['name']}_func_evt")
event["func_evt"] = f"{event['name']}_func_evt"
event["func_evt_desc"] = func_evt
if "func_act" in event:
# replace the name of the function by something unique
func_act = event["func_act"]
func_act = func_act.replace("def func_act", f"def {event['name']}_func_act")
event["func_act"] = f"{event['name']}_func_act"
event["func_act_desc"] = func_act
# if the whole function in defined in the event, make sure it has a unique identifier
if event["func_evt"].startswith("def"):
# replace the name of the function by something unique
func_evt = event["func_evt"]
func_evt = func_evt.replace(
"def func_evt", f"def {event['name']}_func_evt"
)
event["func_evt"] = f"{event['name']}_func_evt"
event["func_evt_desc"] = func_evt
# otherwise assume it was defined in the global namespace
# and just copy the function identifier
else:
event["func_evt_desc"] = event["func_evt"]

if "func_act" in event:
# if the whole function in defined in the event, make sure it has a unique identifier
if event["func_act"].startswith("def"):
# replace the name of the function by something unique
func_act = event["func_act"]
func_act = func_act.replace(
"def func_act", f"def {event['name']}_func_act"
)
event["func_act"] = f"{event['name']}_func_act"
event["func_act_desc"] = func_act
# otherwise assume it was defined in the global namespace
# and just copy the function identifier
else:
event["func_act_desc"] = event["func_act"]
return data["events"]


Expand Down
5 changes: 4 additions & 1 deletion src/python/pathsim_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@
"butterworthbandpass": pathsim.blocks.ButterworthBandpassFilter,
"butterworthbandstop": pathsim.blocks.ButterworthBandstopFilter,
"fir": pathsim.blocks.FIR,
"interface": pathsim.subsystem.Interface,
"switch": pathsim.blocks.Switch,
}

math_blocks = {
Expand Down Expand Up @@ -522,7 +524,7 @@ def get_input_index(block: Block, edge: dict, block_to_input_index: dict) -> int
return edge["targetHandle"]

# TODO maybe we could directly use the targetHandle as a port alias for these:
if type(block) in (Function, ODE):
if type(block) in (Function, ODE, pathsim.blocks.Switch):
return int(edge["targetHandle"].replace("target-", ""))
else:
# make sure that the target block has only one input port (ie. that targetHandle is None)
Expand Down Expand Up @@ -661,6 +663,7 @@ def make_events(events_data: list[dict], eval_namespace: dict = None) -> list[Ev

event = auto_event_construction(event_data, eval_namespace)
events.append(event)
eval_namespace[event_data["name"]] = event
return events


Expand Down
4 changes: 4 additions & 0 deletions src/python/templates/block_macros.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,16 @@

{% macro create_event(event) -%}
{% if "func_evt" in event %}
{% if event["func_evt_desc"].startswith("def") %}
{{ event["func_evt_desc"] }}
{% endif %}
{% endif %}

{% if "func_act" in event %}
{% if event["func_act_desc"].startswith("def") %}
{{ event["func_act_desc"] }}
{% endif %}
{% endif %}

{{ event["name"] }} = {{ event["module_name"] }}.{{ event["class_name"] }}(
{%- for arg in event["expected_arguments"] %}
Expand Down
4 changes: 4 additions & 0 deletions src/python/templates/template_with_macros.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,12 @@
events=events,
Solver=pathsim.solvers.{{ solverParams["Solver"] }},
dt={{ solverParams["dt"] }},
{%- if solverParams["dt_max"] != '' -%}
dt_max={{ solverParams["dt_max"] }},
{%- endif -%}
{%- if solverParams["dt_min"] != '' -%}
dt_min={{ solverParams["dt_min"] }},
{%- endif -%}
iterations_max={{ solverParams["iterations_max"] }},
log={{ solverParams["log"].capitalize() }},
tolerance_fpi={{ solverParams["tolerance_fpi"] }},
Expand Down
26 changes: 26 additions & 0 deletions test/test_examples.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from pathview.pathsim_utils import make_pathsim_model
from pathview.convert_to_python import convert_graph_to_python
import json

import pytest
Expand Down Expand Up @@ -31,3 +32,28 @@ def test_example(filename):

# Run the simulation
my_simulation.run(duration)


@pytest.mark.parametrize(
"filename", all_examples_files, ids=[f.stem for f in all_examples_files]
)
def test_python_scripts(filename):
"""Test the converted python scripts for example simulations."""

if "festim" in filename.stem.lower():
try:
import festim as F
except ImportError:
pytest.skip("Festim examples are not yet supported in this test suite.")

with open(filename, "r") as f:
graph_data = json.load(f)

code = convert_graph_to_python(graph_data)
print(code)
# execute the generated code and check for errors
try:
exec(code)
except Exception as e:
print(f"Error occurred: {e}")
assert False
Loading