Skip to content

Commit 7c3a009

Browse files
Merge branch 'main' into joss
2 parents 4a7cd44 + c63eec2 commit 7c3a009

File tree

9 files changed

+735
-36
lines changed

9 files changed

+735
-36
lines changed

example_graphs/stick_slip.json

Lines changed: 585 additions & 0 deletions
Large diffs are not rendered by default.

src/components/EventsTab.jsx

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ const EventsTab = ({ events, setEvents }) => {
4646
...eventDefaults[initialEventType]
4747
};
4848
});
49-
49+
5050
// State to track if we're editing an existing event
5151
const [editingEventId, setEditingEventId] = useState(null);
5252

@@ -79,21 +79,21 @@ const EventsTab = ({ events, setEvents }) => {
7979
const addEvent = () => {
8080
if (currentEvent.name) {
8181
// Validate required fields based on event type
82-
82+
8383
// For Schedule, func_act is required
8484
if (['Schedule', 'ScheduleList'].includes(currentEvent.type) && !currentEvent.func_act) {
8585
alert('func_act is required for Schedule events');
8686
return;
8787
}
88-
88+
8989
// For other event types, both func_evt and func_act are typically required
9090
if (!['Schedule', 'ScheduleList'].includes(currentEvent.type) && (!currentEvent.func_evt || !currentEvent.func_act)) {
9191
alert('Both func_evt and func_act are required for this event type');
9292
return;
9393
}
9494

9595
setEvents(prev => [...prev, { ...currentEvent, id: Date.now() }]);
96-
96+
9797
// Reset to defaults for current type
9898
const resetDefaults = eventDefaults[currentEvent.type] || {};
9999
setCurrentEvent({
@@ -111,23 +111,23 @@ const EventsTab = ({ events, setEvents }) => {
111111

112112
const saveEditedEvent = () => {
113113
if (currentEvent.name) {
114-
114+
115115
// For Schedule, func_act is required
116116
if (currentEvent.type === 'Schedule' && !currentEvent.func_act) {
117117
alert('func_act is required for Schedule events');
118118
return;
119119
}
120-
120+
121121
// For other event types, both func_evt and func_act are typically required
122122
if (currentEvent.type !== 'Schedule' && (!currentEvent.func_evt || !currentEvent.func_act)) {
123123
alert('Both func_evt and func_act are required for this event type');
124124
return;
125125
}
126126

127-
setEvents(prev => prev.map(event =>
127+
setEvents(prev => prev.map(event =>
128128
event.id === editingEventId ? { ...currentEvent } : event
129129
));
130-
130+
131131
// Reset form and exit edit mode
132132
cancelEdit();
133133
}
@@ -179,7 +179,7 @@ const EventsTab = ({ events, setEvents }) => {
179179
<h2 style={{ color: '#ffffff', marginBottom: '20px' }}>
180180
{editingEventId ? 'Edit Event' : 'Add New Event'}
181181
</h2>
182-
182+
183183
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
184184
<div style={{ width: '100%', maxWidth: '400px' }}>
185185
<label style={{ color: '#ffffff', display: 'block', marginBottom: '8px' }}>
@@ -203,7 +203,7 @@ const EventsTab = ({ events, setEvents }) => {
203203
</div>
204204

205205
<div style={{ width: '100%', maxWidth: '400px' }}>
206-
<label style={{ color: '#ffffff', display: 'block', marginBottom: '8px'}}>
206+
<label style={{ color: '#ffffff', display: 'block', marginBottom: '8px' }}>
207207
Event Type:
208208
</label>
209209
<select
@@ -241,15 +241,15 @@ const EventsTab = ({ events, setEvents }) => {
241241
const defaultValue = typeDefaults[key];
242242
const placeholder = defaultValue !== undefined && defaultValue !== null ?
243243
String(defaultValue) : '';
244-
244+
245245
// Check if this is a function parameter (contains 'func' in the name)
246246
const isFunctionParam = key.toLowerCase().includes('func');
247247

248248
return (
249249
<div key={key} style={{ width: '100%', maxWidth: isFunctionParam ? '600px' : '400px' }}>
250-
<label style={{
251-
color: '#ffffff',
252-
display: 'block',
250+
<label style={{
251+
color: '#ffffff',
252+
display: 'block',
253253
marginBottom: '8px',
254254
fontSize: '14px',
255255
fontWeight: '500',
@@ -354,7 +354,7 @@ const EventsTab = ({ events, setEvents }) => {
354354
{/* Events List */}
355355
<div style={{ maxWidth: '800px', margin: '0 auto' }}>
356356
<h2 style={{ color: '#ffffff', marginBottom: '20px', textAlign: 'center' }}>Defined Events ({events.length})</h2>
357-
357+
358358
{events.length === 0 ? (
359359
<div style={{
360360
backgroundColor: '#2a2a3f',
@@ -385,9 +385,9 @@ const EventsTab = ({ events, setEvents }) => {
385385
{event.name} ({event.type})
386386
</h3>
387387
{editingEventId === event.id && (
388-
<span style={{
389-
color: '#007bff',
390-
fontSize: '12px',
388+
<span style={{
389+
color: '#007bff',
390+
fontSize: '12px',
391391
fontStyle: 'italic',
392392
marginTop: '4px',
393393
display: 'block'
@@ -434,12 +434,12 @@ const EventsTab = ({ events, setEvents }) => {
434434
.filter(([key]) => key !== 'id' && key !== 'name' && key !== 'type')
435435
.map(([key, value]) => {
436436
const isFunctionParam = key.toLowerCase().includes('func');
437-
437+
438438
return (
439439
<div key={key}>
440-
<h4 style={{
441-
color: '#ccc',
442-
margin: '0 0 8px 0',
440+
<h4 style={{
441+
color: '#ccc',
442+
margin: '0 0 8px 0',
443443
fontSize: '14px',
444444
textTransform: 'capitalize'
445445
}}>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React from 'react';
2+
import { Handle } from '@xyflow/react';
3+
4+
export default function SwitchNode({ data }) {
5+
return (
6+
<div
7+
style={{
8+
width: 180,
9+
background: data.nodeColor || '#DDE6ED',
10+
color: 'black',
11+
borderRadius: 0,
12+
padding: 10,
13+
fontWeight: 'bold',
14+
position: 'relative',
15+
cursor: 'pointer',
16+
}}
17+
>
18+
<div style={{ marginBottom: 4, marginLeft: 20}}>{data.label}</div>
19+
20+
<Handle type="target" id="target-0" position="left" style={{ background: '#555', top: "33%" }} />
21+
<div
22+
style={{
23+
position: 'absolute',
24+
left: '8px',
25+
top: `33%`,
26+
transform: 'translateY(-50%)',
27+
fontSize: '10px',
28+
fontWeight: 'normal',
29+
color: '#666',
30+
pointerEvents: 'none',
31+
}}
32+
>
33+
0
34+
</div>
35+
<Handle type="target" id="target-1" position="left" style={{ background: '#555', top: "66%" }} />
36+
37+
<div
38+
style={{
39+
position: 'absolute',
40+
left: '8px',
41+
top: `66%`,
42+
transform: 'translateY(-50%)',
43+
fontSize: '10px',
44+
fontWeight: 'normal',
45+
color: '#666',
46+
pointerEvents: 'none',
47+
}}
48+
>
49+
1
50+
</div>
51+
<Handle type="source" position="right" style={{ background: '#555' }} />
52+
</div>
53+
);
54+
}

src/nodeConfig.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Splitter2Node, Splitter3Node } from './components/nodes/Splitters';
1414
import BubblerNode from './components/nodes/BubblerNode';
1515
import WallNode from './components/nodes/WallNode';
1616
import { DynamicHandleNode } from './components/nodes/DynamicHandleNode';
17+
import SwitchNode from './components/nodes/SwitchNode';
1718

1819
// Node types mapping
1920
export const nodeTypes = {
@@ -59,6 +60,8 @@ export const nodeTypes = {
5960
butterworthbandstop: DefaultNode,
6061
fir: DefaultNode,
6162
ode: DynamicHandleNode,
63+
interface: DynamicHandleNode,
64+
switch: SwitchNode,
6265
};
6366

6467
export const nodeMathTypes = {
@@ -87,7 +90,7 @@ Object.keys(nodeMathTypes).forEach(type => {
8790
}
8891
});
8992

90-
export const nodeDynamicHandles = ['ode', 'function'];
93+
export const nodeDynamicHandles = ['ode', 'function', 'interface'];
9194

9295
// Node categories for better organization
9396
export const nodeCategories = {
@@ -116,7 +119,7 @@ export const nodeCategories = {
116119
description: 'Fuel cycle specific nodes'
117120
},
118121
'Others': {
119-
nodes: ['samplehold', 'comparator'],
122+
nodes: ['samplehold', 'comparator', 'switch', 'interface'],
120123
description: 'Miscellaneous nodes'
121124
},
122125
'Output': {
@@ -184,6 +187,10 @@ export const getNodeDisplayName = (nodeType) => {
184187
'butterworthbandpass': 'Butterworth Band-Pass Filter',
185188
'butterworthbandstop': 'Butterworth Band-Stop Filter',
186189
'fir': 'FIR Filter',
190+
'switch': 'Switch',
191+
'samplehold': 'Sample Hold',
192+
'comparator': 'Comparator',
193+
'interface': 'Interface',
187194
};
188195

189196
return displayNames[nodeType] || nodeType.charAt(0).toUpperCase() + nodeType.slice(1);

src/python/convert_to_python.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -160,18 +160,34 @@ def make_events_data(data: dict) -> list[dict]:
160160
event["expected_arguments"] = signature(event_class).parameters
161161

162162
if "func_evt" in event:
163-
# replace the name of the function by something unique
164-
func_evt = event["func_evt"]
165-
func_evt = func_evt.replace("def func_evt", f"def {event['name']}_func_evt")
166-
event["func_evt"] = f"{event['name']}_func_evt"
167-
event["func_evt_desc"] = func_evt
168-
if "func_act" in event:
169-
# replace the name of the function by something unique
170-
func_act = event["func_act"]
171-
func_act = func_act.replace("def func_act", f"def {event['name']}_func_act")
172-
event["func_act"] = f"{event['name']}_func_act"
173-
event["func_act_desc"] = func_act
163+
# if the whole function in defined in the event, make sure it has a unique identifier
164+
if event["func_evt"].startswith("def"):
165+
# replace the name of the function by something unique
166+
func_evt = event["func_evt"]
167+
func_evt = func_evt.replace(
168+
"def func_evt", f"def {event['name']}_func_evt"
169+
)
170+
event["func_evt"] = f"{event['name']}_func_evt"
171+
event["func_evt_desc"] = func_evt
172+
# otherwise assume it was defined in the global namespace
173+
# and just copy the function identifier
174+
else:
175+
event["func_evt_desc"] = event["func_evt"]
174176

177+
if "func_act" in event:
178+
# if the whole function in defined in the event, make sure it has a unique identifier
179+
if event["func_act"].startswith("def"):
180+
# replace the name of the function by something unique
181+
func_act = event["func_act"]
182+
func_act = func_act.replace(
183+
"def func_act", f"def {event['name']}_func_act"
184+
)
185+
event["func_act"] = f"{event['name']}_func_act"
186+
event["func_act_desc"] = func_act
187+
# otherwise assume it was defined in the global namespace
188+
# and just copy the function identifier
189+
else:
190+
event["func_act_desc"] = event["func_act"]
175191
return data["events"]
176192

177193

src/python/pathsim_utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@
132132
"butterworthbandpass": pathsim.blocks.ButterworthBandpassFilter,
133133
"butterworthbandstop": pathsim.blocks.ButterworthBandstopFilter,
134134
"fir": pathsim.blocks.FIR,
135+
"interface": pathsim.subsystem.Interface,
136+
"switch": pathsim.blocks.Switch,
135137
}
136138

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

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

662664
event = auto_event_construction(event_data, eval_namespace)
663665
events.append(event)
666+
eval_namespace[event_data["name"]] = event
664667
return events
665668

666669

src/python/templates/block_macros.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,16 @@
4242

4343
{% macro create_event(event) -%}
4444
{% if "func_evt" in event %}
45+
{% if event["func_evt_desc"].startswith("def") %}
4546
{{ event["func_evt_desc"] }}
4647
{% endif %}
48+
{% endif %}
4749

4850
{% if "func_act" in event %}
51+
{% if event["func_act_desc"].startswith("def") %}
4952
{{ event["func_act_desc"] }}
5053
{% endif %}
54+
{% endif %}
5155

5256
{{ event["name"] }} = {{ event["module_name"] }}.{{ event["class_name"] }}(
5357
{%- for arg in event["expected_arguments"] %}

src/python/templates/template_with_macros.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,12 @@
4747
events=events,
4848
Solver=pathsim.solvers.{{ solverParams["Solver"] }},
4949
dt={{ solverParams["dt"] }},
50+
{%- if solverParams["dt_max"] != '' -%}
5051
dt_max={{ solverParams["dt_max"] }},
52+
{%- endif -%}
53+
{%- if solverParams["dt_min"] != '' -%}
5154
dt_min={{ solverParams["dt_min"] }},
55+
{%- endif -%}
5256
iterations_max={{ solverParams["iterations_max"] }},
5357
log={{ solverParams["log"].capitalize() }},
5458
tolerance_fpi={{ solverParams["tolerance_fpi"] }},

test/test_examples.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from pathview.pathsim_utils import make_pathsim_model
2+
from pathview.convert_to_python import convert_graph_to_python
23
import json
34

45
import pytest
@@ -31,3 +32,28 @@ def test_example(filename):
3132

3233
# Run the simulation
3334
my_simulation.run(duration)
35+
36+
37+
@pytest.mark.parametrize(
38+
"filename", all_examples_files, ids=[f.stem for f in all_examples_files]
39+
)
40+
def test_python_scripts(filename):
41+
"""Test the converted python scripts for example simulations."""
42+
43+
if "festim" in filename.stem.lower():
44+
try:
45+
import festim as F
46+
except ImportError:
47+
pytest.skip("Festim examples are not yet supported in this test suite.")
48+
49+
with open(filename, "r") as f:
50+
graph_data = json.load(f)
51+
52+
code = convert_graph_to_python(graph_data)
53+
print(code)
54+
# execute the generated code and check for errors
55+
try:
56+
exec(code)
57+
except Exception as e:
58+
print(f"Error occurred: {e}")
59+
assert False

0 commit comments

Comments
 (0)