diff --git a/example_graphs/spectrum.json b/example_graphs/spectrum.json new file mode 100644 index 00000000..d10b08b7 --- /dev/null +++ b/example_graphs/spectrum.json @@ -0,0 +1,188 @@ +{ + "nodes": [ + { + "id": "0", + "type": "spectrum", + "position": { + "x": 487.91666412353516, + "y": 247.4166660308838 + }, + "data": { + "label": "spectrum 0", + "alpha": "", + "freq": "np.linspace(0, 1*np.pi, 600)", + "labels": "", + "t_wait": "" + }, + "measured": { + "width": 120, + "height": 140 + }, + "selected": false, + "dragging": false + }, + { + "id": "1", + "type": "source", + "position": { + "x": 101.91666412353516, + "y": 210.4166660308838 + }, + "data": { + "label": "High frequency", + "func": "lambda t: np.sin(1.5*t *2*np.pi)" + }, + "measured": { + "width": 205, + "height": 53 + }, + "selected": false, + "dragging": false + }, + { + "id": "2", + "type": "scope", + "position": { + "x": 479.5833282470703, + "y": 446.0833320617676 + }, + "data": { + "label": "scope 2", + "labels": "", + "sampling_rate": "", + "t_wait": "" + }, + "measured": { + "width": 120, + "height": 140 + }, + "selected": false, + "dragging": false + }, + { + "id": "3", + "type": "source", + "position": { + "x": -34.083335876464844, + "y": 299.4166660308838 + }, + "data": { + "label": "Low frequency", + "func": "lambda t: np.sin(0.5*t * 2*np.pi)" + }, + "measured": { + "width": 205, + "height": 53 + }, + "selected": false, + "dragging": false + }, + { + "id": "4", + "type": "adder", + "position": { + "x": 283.1666603088379, + "y": 449.4166650772095 + }, + "data": { + "label": "adder 4", + "operations": "" + }, + "measured": { + "width": 64, + "height": 64 + } + } + ], + "edges": [ + { + "id": "e3-4", + "source": "3", + "target": "4", + "sourceHandle": null, + "targetHandle": null, + "type": "smoothstep", + "data": {}, + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } + }, + { + "id": "e4-0", + "source": "4", + "target": "0", + "sourceHandle": null, + "targetHandle": null, + "type": "smoothstep", + "data": {}, + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } + }, + { + "id": "e4-2", + "source": "4", + "target": "2", + "sourceHandle": null, + "targetHandle": null, + "type": "smoothstep", + "data": {}, + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } + }, + { + "id": "e1-4", + "source": "1", + "target": "4", + "sourceHandle": null, + "targetHandle": null, + "type": "smoothstep", + "data": {}, + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } + } + ], + "nodeCounter": 5, + "solverParams": { + "dt": "0.01", + "dt_min": "1e-6", + "dt_max": "1.0", + "Solver": "SSPRK22", + "tolerance_fpi": "1e-6", + "iterations_max": "100", + "log": "true", + "simulation_duration": "2*2*np.pi", + "extra_params": "{}" + }, + "globalVariables": [] +} \ No newline at end of file diff --git a/mwe.py b/mwe.py deleted file mode 100644 index c52da7f8..00000000 --- a/mwe.py +++ /dev/null @@ -1,32 +0,0 @@ -from src.custom_pathsim_blocks import FestimWall - -from pathsim import Simulation, Connection -import pathsim.blocks -import numpy as np -import matplotlib.pyplot as plt - - -# Create blocks -blocks, events = [], [] - -# Create Festim wall -festim_wall = FestimWall(thickness=1, D_0=1, E_D=0, temperature=300, n_vertices=100) -source_c0 = pathsim.blocks.Constant(0) -source_cL = pathsim.blocks.Constant(1) - -scope = pathsim.blocks.Scope(labels=["flux_0", "flux_L"]) - -blocks = [festim_wall, source_c0, source_cL, scope] - -connections = [ - Connection(source_c0, festim_wall[0]), - Connection(source_cL, festim_wall[1]), - Connection(festim_wall[0], scope[0]), - Connection(festim_wall[1], scope[1]), -] - -simulation = Simulation(blocks=blocks, connections=connections, dt=0.005) -simulation.run(0.1) - -scope.plot(marker="o") -plt.show() diff --git a/src/backend.py b/src/backend.py index 3b26dff9..047e1781 100644 --- a/src/backend.py +++ b/src/backend.py @@ -8,10 +8,11 @@ import plotly import json as plotly_json import inspect +import numpy as np from .convert_to_python import convert_graph_to_python from .pathsim_utils import make_pathsim_model, map_str_to_object -from pathsim.blocks import Scope +from pathsim.blocks import Scope, Spectrum # Sphinx imports for docstring processing from docutils.core import publish_parts @@ -254,47 +255,55 @@ def run_pathsim(): # Generate the plot scopes = [block for block in my_simulation.blocks if isinstance(block, Scope)] + spectra = [ + block for block in my_simulation.blocks if isinstance(block, Spectrum) + ] + # FIXME right now only the scopes are converted to CSV + # extra work is needed since spectra and scopes don't share the same x axis csv_payload = make_csv_payload(scopes) - if len(scopes) == 1: - # Single subplot case - fig = go.Figure() - scope = scopes[0] + fig = make_subplots( + rows=len(scopes) + len(spectra), + cols=1, + shared_xaxes=False, + subplot_titles=[scope.label for scope in scopes] + + [spec.label for spec in spectra], + vertical_spacing=0.2, + ) + + # make scope plots + for i, scope in enumerate(scopes): time, data = scope.read() for p, d in enumerate(data): lb = scope.labels[p] if p < len(scope.labels) else f"port {p}" - fig.add_trace(go.Scatter(x=time, y=d, mode="lines", name=lb)) - - fig.update_layout( - title=scope.label, - xaxis_title="Time", - yaxis_title="Value", - hovermode="x unified", - ) - else: - # Multiple subplots case - fig = make_subplots( - rows=len(scopes), - cols=1, - shared_xaxes=True, - subplot_titles=[scope.label for scope in scopes], - vertical_spacing=0.1, - ) - - for i, scope in enumerate(scopes): - time, data = scope.read() - - for p, d in enumerate(data): - lb = scope.labels[p] if p < len(scope.labels) else f"port {p}" - fig.add_trace( - go.Scatter(x=time, y=d, mode="lines", name=lb), row=i + 1, col=1 - ) - - fig.update_layout(height=400 * len(scopes), hovermode="x unified") + if isinstance(scope, Spectrum): + d = abs(d) + fig.add_trace( + go.Scatter(x=time, y=d, mode="lines", name=lb), row=i + 1, col=1 + ) + fig.update_xaxes(title_text="Time", row=len(scopes), col=1) + # make spectrum plots + for i, spec in enumerate(spectra): + time, data = spec.read() + + for p, d in enumerate(data): + lb = spec.labels[p] if p < len(spec.labels) else f"port {p}" + d = abs(d) + fig.add_trace( + go.Scatter(x=time, y=d, mode="lines", name=lb), + row=len(scopes) + i + 1, + col=1, + ) + fig.update_xaxes(title_text="Frequency", row=len(scopes) + i + 1, col=1) + + fig.update_layout( + height=400 * (len(scopes) + len(spectra)), hovermode="x unified" + ) + # Convert plot to JSON plot_data = plotly_json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) diff --git a/src/nodeConfig.js b/src/nodeConfig.js index 9814f155..4f286695 100644 --- a/src/nodeConfig.js +++ b/src/nodeConfig.js @@ -39,6 +39,7 @@ export const nodeTypes = { bubbler: BubblerNode, white_noise: SourceNode, pink_noise: SourceNode, + spectrum: ScopeNode, differentiator: DefaultNode }; @@ -65,7 +66,7 @@ export const nodeCategories = { description: 'Fuel cycle specific nodes' }, 'Output': { - nodes: ['scope'], + nodes: ['scope', 'spectrum'], description: 'Output and visualization nodes' } }; @@ -96,6 +97,7 @@ export const getNodeDisplayName = (nodeType) => { 'bubbler': 'Bubbler', 'wall': 'Wall', 'scope': 'Scope', + 'spectrum': 'Spectrum', 'differentiator': 'Differentiator', }; diff --git a/src/pathsim_utils.py b/src/pathsim_utils.py index 617c78b1..b98796e9 100644 --- a/src/pathsim_utils.py +++ b/src/pathsim_utils.py @@ -17,6 +17,7 @@ Delay, RNG, PID, + Spectrum, Differentiator, Schedule, ) @@ -64,6 +65,7 @@ "wall": FestimWall, "white_noise": WhiteNoise, "pink_noise": PinkNoise, + "spectrum": Spectrum, } @@ -78,7 +80,7 @@ def find_block_by_id(block_id: str, blocks: list[Block]) -> Block: def make_global_variables(global_vars): # Validate and exec global variables so that they are usable later in this script. # Return a namespace dictionary containing the global variables - global_namespace = {} + global_namespace = globals() for var in global_vars: var_name = var.get("name", "").strip() @@ -105,7 +107,7 @@ def make_global_variables(global_vars): try: # Execute in global namespace for backwards compatibility - exec(f"{var_name} = {var_value}", globals()) + exec(f"{var_name} = {var_value}", global_namespace) # Also store in local namespace for eval calls global_namespace[var_name] = eval(var_value) except Exception as e: @@ -313,7 +315,7 @@ def make_connections(nodes, edges, blocks) -> list[Connection]: input_index = get_input_index(target_block, edge, block_to_input_index) # if it's a scope, add labels if not already present - if isinstance(target_block, Scope): + if isinstance(target_block, (Scope, Spectrum)): if target_block.labels == []: scopes_without_labels.append(target_block) if target_block in scopes_without_labels: diff --git a/src/templates/block_macros.py b/src/templates/block_macros.py index ff4d2365..ce20e8f5 100644 --- a/src/templates/block_macros.py +++ b/src/templates/block_macros.py @@ -32,13 +32,6 @@ {%- endmacro -%} -{% macro create_scope_block(node) -%} -{{ node["var_name"] }} = pathsim.blocks.Scope( - labels={{ node["data"]["labels"] }} -) - -{%- endmacro -%} - {% macro create_connections(edges) -%} connections = [ {% for edge in edges -%} diff --git a/src/templates/template_with_macros.py b/src/templates/template_with_macros.py index 2e7344c0..f30cc03f 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_scope_block, create_bubbler_block, create_connections -%} +{% from 'block_macros.py' import create_block, create_integrator_block, create_bubbler_block, create_connections -%} # Create global variables {% for var in globalVariables -%} @@ -16,11 +16,8 @@ {% for node in nodes -%} {%- if node["type"] == "integrator" -%} {{ create_integrator_block(node) }} -{%- elif node["type"] == "scope" -%} -{{ create_scope_block(node) }} {%- elif node["type"] == "bubbler" -%} {{ create_bubbler_block(node) }} -{%- elif node["type"] == "source" -%} {%- else -%} {{ create_block(node) }} {%- endif %} diff --git a/test/test_convert_python.py b/test/test_convert_python.py index 2680068f..f20b9b50 100644 --- a/test/test_convert_python.py +++ b/test/test_convert_python.py @@ -106,6 +106,7 @@ "test_files/custom_nodes.json", "test_files/same_label.json", "test_files/bubbler.json", + "test_files/spectrum.json", ], ) def test_nested_templates(data): diff --git a/test/test_files/spectrum.json b/test/test_files/spectrum.json new file mode 100644 index 00000000..904c8ed8 --- /dev/null +++ b/test/test_files/spectrum.json @@ -0,0 +1,188 @@ +{ + "nodes": [ + { + "id": "0", + "type": "spectrum", + "position": { + "x": 487.91666412353516, + "y": 247.4166660308838 + }, + "data": { + "label": "spectrum 0", + "alpha": "", + "freq": "np.linspace(0, 1*np.pi, 600)", + "labels": "", + "t_wait": "" + }, + "measured": { + "width": 120, + "height": 140 + }, + "selected": true, + "dragging": false + }, + { + "id": "1", + "type": "source", + "position": { + "x": 101.91666412353516, + "y": 210.4166660308838 + }, + "data": { + "label": "High frequency", + "func": "lambda t: np.sin(1.5*t *2*np.pi)" + }, + "measured": { + "width": 205, + "height": 53 + }, + "selected": false, + "dragging": false + }, + { + "id": "2", + "type": "scope", + "position": { + "x": 479.5833282470703, + "y": 446.0833320617676 + }, + "data": { + "label": "scope 2", + "labels": "", + "sampling_rate": "", + "t_wait": "" + }, + "measured": { + "width": 120, + "height": 140 + }, + "selected": false, + "dragging": false + }, + { + "id": "3", + "type": "source", + "position": { + "x": -34.083335876464844, + "y": 299.4166660308838 + }, + "data": { + "label": "Low frequency", + "func": "lambda t: np.sin(0.5*t * 2*np.pi)" + }, + "measured": { + "width": 205, + "height": 53 + }, + "selected": false, + "dragging": false + }, + { + "id": "4", + "type": "adder", + "position": { + "x": 283.1666603088379, + "y": 449.4166650772095 + }, + "data": { + "label": "adder 4", + "operations": "" + }, + "measured": { + "width": 64, + "height": 64 + } + } + ], + "edges": [ + { + "id": "e3-4", + "source": "3", + "target": "4", + "sourceHandle": null, + "targetHandle": null, + "type": "smoothstep", + "data": {}, + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } + }, + { + "id": "e4-0", + "source": "4", + "target": "0", + "sourceHandle": null, + "targetHandle": null, + "type": "smoothstep", + "data": {}, + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } + }, + { + "id": "e4-2", + "source": "4", + "target": "2", + "sourceHandle": null, + "targetHandle": null, + "type": "smoothstep", + "data": {}, + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } + }, + { + "id": "e1-4", + "source": "1", + "target": "4", + "sourceHandle": null, + "targetHandle": null, + "type": "smoothstep", + "data": {}, + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } + } + ], + "nodeCounter": 5, + "solverParams": { + "dt": "0.01", + "dt_min": "1e-6", + "dt_max": "1.0", + "Solver": "SSPRK22", + "tolerance_fpi": "1e-6", + "iterations_max": "100", + "log": "true", + "simulation_duration": "2*2*np.pi", + "extra_params": "{}" + }, + "globalVariables": [] +} \ No newline at end of file