From ccca85e4b0e3697cdf0c5e6b38811226e4641ee6 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Thu, 31 Jul 2025 14:39:31 -0400 Subject: [PATCH 1/5] initial custom block --- mwe.py | 32 ++++++++++++ src/custom_pathsim_blocks.py | 96 ++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 mwe.py diff --git a/mwe.py b/mwe.py new file mode 100644 index 00000000..c52da7f8 --- /dev/null +++ b/mwe.py @@ -0,0 +1,32 @@ +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/custom_pathsim_blocks.py b/src/custom_pathsim_blocks.py index a7784c08..2c9d29de 100644 --- a/src/custom_pathsim_blocks.py +++ b/src/custom_pathsim_blocks.py @@ -170,3 +170,99 @@ def create_reset_events(self) -> list[pathsim.blocks.Schedule]: for i, vial in enumerate(self.vials): events.extend(self._create_reset_events_one_vial(vial, reset_times[i])) return events + + +# FESTIM wall + + +class FestimWall(Block): + def __init__( + self, thickness, temperature, D_0, E_D, n_vertices=100, final_time=100 + ): + super().__init__() + try: + import festim as F + except ImportError: + raise ImportError("festim is needed for FestimWall node.") + + self.thickness = thickness + self.temperature = temperature + self.D_0 = D_0 + self.E_D = E_D + self.n_vertices = n_vertices + self.final_time = final_time + self.t = 0.0 + + self.initialise_festim_model() + + def initialise_festim_model(self): + import festim as F + + model = F.HydrogenTransportProblem() + + model.mesh = F.Mesh1D( + vertices=np.linspace(0, self.thickness, num=self.n_vertices) + ) + material = F.Material(D_0=self.D_0, E_D=self.E_D) + + vol = F.VolumeSubdomain1D(id=1, material=material, borders=[0, self.thickness]) + left_surf = F.SurfaceSubdomain1D(id=1, x=0) + right_surf = F.SurfaceSubdomain1D(id=2, x=self.thickness) + + model.subdomains = [vol, left_surf, right_surf] + + H = F.Species("H") + model.species = [H] + + model.boundary_conditions = [ + F.FixedConcentrationBC(left_surf, value=0.0, species=H), + F.FixedConcentrationBC(right_surf, value=0.0, species=H), + ] + + model.temperature = self.temperature + + model.settings = F.Settings( + atol=1e-10, rtol=1e-10, transient=True, final_time=self.final_time + ) + + model.settings.stepsize = F.Stepsize(initial_value=1) + + self.surface_flux_0 = F.SurfaceFlux(field=H, surface=left_surf) + self.surface_flux_L = F.SurfaceFlux(field=H, surface=right_surf) + model.exports = [self.surface_flux_0, self.surface_flux_L] + + model.show_progress_bar = False + + model.initialise() + + self.dt = model.dt + self.c_0 = model.boundary_conditions[0].value_fenics + self.c_L = model.boundary_conditions[1].value_fenics + + self.model = model + + def update_festim_model(self, c_0, c_L, stepsize): + self.c_0.value = c_0 + self.c_L.value = c_L + self.dt.value = stepsize + + self.model.iterate() + + return self.surface_flux_0.data[-1], self.surface_flux_L.data[-1] + + def update(self, t): + # no internal algebraic operator -> early exit + # if self.op_alg is None: + # return 0.0 + + # block inputs + c_0, c_L = self.inputs.to_array() + + if t == 0.0: + flux_0, flux_L = 0, 0 + else: + flux_0, flux_L = self.update_festim_model( + c_0=c_0, c_L=c_L, stepsize=t - self.t + ) + # error control + return self.outputs.update_from_array([flux_0, flux_L]) From 3adea7c249b29137ba1c615a65fb6d8dcb51200a Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Thu, 31 Jul 2025 14:49:07 -0400 Subject: [PATCH 2/5] added node --- src/App.jsx | 4 +++ src/WallNode.jsx | 68 ++++++++++++++++++++++++++++++++++++ src/custom_pathsim_blocks.py | 5 +-- 3 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 src/WallNode.jsx diff --git a/src/App.jsx b/src/App.jsx index 2cdbaecf..883653af 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -28,6 +28,7 @@ import { makeEdge } from './CustomEdge'; import MultiplierNode from './MultiplierNode'; import { Splitter2Node, Splitter3Node } from './Splitters'; import BubblerNode from './BubblerNode'; +import WallNode from './WallNode'; // Add nodes as a node type for this script const nodeTypes = { @@ -48,6 +49,7 @@ const nodeTypes = { pid: DefaultNode, splitter2: Splitter2Node, splitter3: Splitter3Node, + wall: WallNode, bubbler: BubblerNode, }; @@ -600,6 +602,8 @@ export default function App() { case 'splitter3': nodeData = { ...nodeData, f1: '1/3', f2: '1/3', f3: '1/3' }; break; + case 'wall': + nodeData = { ...nodeData, thickness: '', temperature: '', D_0: '1', E_D: '0', n_vertices: '100' }; case 'bubbler': nodeData = { ...nodeData, conversion_efficiency: '0.95', vial_efficiency: '0.9', replacement_time: '' }; default: diff --git a/src/WallNode.jsx b/src/WallNode.jsx new file mode 100644 index 00000000..3827d5ed --- /dev/null +++ b/src/WallNode.jsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { Handle } from '@xyflow/react'; + +export default function WallNode({ data }) { + return ( +
+
{data.label}
+ + {/* Left side handles with labels */} + +
+ c₀ +
+ + +
+ φ₀ +
+ + {/* Right side handles with labels */} + +
+ cₗ +
+ + +
+ φₗ +
+
+ ); +} diff --git a/src/custom_pathsim_blocks.py b/src/custom_pathsim_blocks.py index 2c9d29de..77b1a3ca 100644 --- a/src/custom_pathsim_blocks.py +++ b/src/custom_pathsim_blocks.py @@ -176,9 +176,7 @@ def create_reset_events(self) -> list[pathsim.blocks.Schedule]: class FestimWall(Block): - def __init__( - self, thickness, temperature, D_0, E_D, n_vertices=100, final_time=100 - ): + def __init__(self, thickness, temperature, D_0, E_D, n_vertices=100): super().__init__() try: import festim as F @@ -190,7 +188,6 @@ def __init__( self.D_0 = D_0 self.E_D = E_D self.n_vertices = n_vertices - self.final_time = final_time self.t = 0.0 self.initialise_festim_model() From 4849fbad684019eb5958d5cd8a86a11770915a82 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Thu, 31 Jul 2025 15:10:27 -0400 Subject: [PATCH 3/5] integration with front end + python scripting --- src/App.jsx | 2 ++ src/convert_to_python.py | 24 +++++++++++++++++++----- src/custom_pathsim_blocks.py | 2 +- src/pathsim_utils.py | 22 +++++++++++++++++++++- 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 611023fa..57468e8c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -610,8 +610,10 @@ export default function App() { break; case 'wall': nodeData = { ...nodeData, thickness: '', temperature: '', D_0: '1', E_D: '0', n_vertices: '100' }; + break; case 'bubbler': nodeData = { ...nodeData, conversion_efficiency: '0.95', vial_efficiency: '0.9', replacement_times: '' }; + break; default: // For any other types, just use basic data break; diff --git a/src/convert_to_python.py b/src/convert_to_python.py index 29b45b75..e1e1aa2b 100644 --- a/src/convert_to_python.py +++ b/src/convert_to_python.py @@ -3,11 +3,7 @@ from inspect import signature from pathsim.blocks import Scope -from .custom_pathsim_blocks import ( - Process, - Splitter, - Bubbler, -) +from .custom_pathsim_blocks import Process, Splitter, Bubbler, FestimWall from .pathsim_utils import ( map_str_to_object, make_blocks, @@ -162,11 +158,29 @@ def make_edge_data(data: dict) -> list[dict]: raise ValueError( f"Invalid source handle '{edge['sourceHandle']}' for {edge}." ) + elif isinstance(block, FestimWall): + if edge["sourceHandle"] == "flux_0": + output_index = 0 + elif edge["sourceHandle"] == "flux_L": + output_index = 1 + else: + raise ValueError( + f"Invalid source handle '{edge['sourceHandle']}' for {edge}." + ) else: output_index = 0 if isinstance(target_block, Scope): input_index = target_block._connections_order.index(edge["id"]) + elif isinstance(target_block, FestimWall): + if edge["targetHandle"] == "c_0": + input_index = 0 + elif edge["targetHandle"] == "c_L": + input_index = 1 + else: + raise ValueError( + f"Invalid target handle '{edge['targetHandle']}' for {edge}." + ) else: input_index = block_to_input_index[target_block] diff --git a/src/custom_pathsim_blocks.py b/src/custom_pathsim_blocks.py index 17f12067..d1dca5cd 100644 --- a/src/custom_pathsim_blocks.py +++ b/src/custom_pathsim_blocks.py @@ -220,7 +220,7 @@ def initialise_festim_model(self): model.temperature = self.temperature model.settings = F.Settings( - atol=1e-10, rtol=1e-10, transient=True, final_time=self.final_time + atol=1e-10, rtol=1e-10, transient=True, final_time=1 ) model.settings.stepsize = F.Stepsize(initial_value=1) diff --git a/src/pathsim_utils.py b/src/pathsim_utils.py index f18378c7..a46892cf 100644 --- a/src/pathsim_utils.py +++ b/src/pathsim_utils.py @@ -19,7 +19,7 @@ PID, Schedule, ) -from .custom_pathsim_blocks import Process, Splitter, Bubbler +from .custom_pathsim_blocks import Process, Splitter, Bubbler, FestimWall from flask import jsonify NAME_TO_SOLVER = { @@ -47,6 +47,7 @@ "function": Function, "delay": Delay, "bubbler": Bubbler, + "wall": FestimWall, } @@ -410,11 +411,30 @@ def make_connections(nodes, edges, blocks) -> list[Connection]: raise ValueError( f"Invalid source handle '{edge['sourceHandle']}' for {edge}." ) + elif isinstance(block, FestimWall): + if edge["sourceHandle"] == "flux_0": + output_index = 0 + elif edge["sourceHandle"] == "flux_L": + output_index = 1 + else: + raise ValueError( + f"Invalid source handle '{edge['sourceHandle']}' for {edge}." + ) else: output_index = 0 if isinstance(target_block, Scope): input_index = target_block._connections_order.index(edge["id"]) + # TODO we should do the same for all blocks with several input/target ports + elif isinstance(target_block, FestimWall): + if edge["targetHandle"] == "c_0": + input_index = 0 + elif edge["targetHandle"] == "c_L": + input_index = 1 + else: + raise ValueError( + f"Invalid target handle '{edge['targetHandle']}' for {edge}." + ) else: input_index = block_to_input_index[target_block] From e6b1a2bdcb743f95c50883266e0d8038526102b1 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Thu, 31 Jul 2025 15:22:00 -0400 Subject: [PATCH 4/5] added example festim --- saved_graphs/festim_example.json | 207 +++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 saved_graphs/festim_example.json diff --git a/saved_graphs/festim_example.json b/saved_graphs/festim_example.json new file mode 100644 index 00000000..c247870c --- /dev/null +++ b/saved_graphs/festim_example.json @@ -0,0 +1,207 @@ +{ + "nodes": [ + { + "id": "2", + "type": "wall", + "position": { + "x": 919.6228024037504, + "y": 398.2293552785672 + }, + "data": { + "label": "wall 2", + "thickness": "1", + "temperature": "300", + "D_0": "0.01", + "E_D": "0", + "n_vertices": "100" + }, + "measured": { + "width": 70, + "height": 200 + }, + "selected": false, + "dragging": false + }, + { + "id": "4", + "type": "constant", + "position": { + "x": 1093.1885987981248, + "y": 271.20138523630243 + }, + "data": { + "label": "cL", + "value": "0" + }, + "measured": { + "width": 205, + "height": 53 + }, + "selected": false, + "dragging": false + }, + { + "id": "5", + "type": "scope", + "position": { + "x": 1271, + "y": 457 + }, + "data": { + "label": "scope 5" + }, + "measured": { + "width": 120, + "height": 140 + }, + "selected": true, + "dragging": false + }, + { + "id": "9", + "type": "scope", + "position": { + "x": 967.9637725351604, + "y": 81.60628755860085 + }, + "data": { + "label": "scope 9" + }, + "measured": { + "width": 120, + "height": 140 + }, + "selected": false, + "dragging": false + }, + { + "id": "10", + "type": "stepsource", + "position": { + "x": 589, + "y": 224 + }, + "data": { + "label": "stepsource 10", + "amplitude": "1", + "delay": "0.2" + }, + "measured": { + "width": 120, + "height": 120 + }, + "selected": false, + "dragging": false + } + ], + "edges": [ + { + "id": "e4-2-to_c_L", + "source": "4", + "target": "2", + "sourceHandle": null, + "targetHandle": "c_L", + "type": "smoothstep", + "data": {}, + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } + }, + { + "id": "e2-5-from_flux_L", + "source": "2", + "target": "5", + "sourceHandle": "flux_L", + "targetHandle": null, + "type": "smoothstep", + "data": {}, + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } + }, + { + "id": "e4-9", + "source": "4", + "target": "9", + "sourceHandle": null, + "targetHandle": null, + "type": "smoothstep", + "data": {}, + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } + }, + { + "id": "e10-2-to_c_0", + "source": "10", + "target": "2", + "sourceHandle": null, + "targetHandle": "c_0", + "type": "smoothstep", + "data": {}, + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } + }, + { + "id": "e10-9", + "source": "10", + "target": "9", + "sourceHandle": null, + "targetHandle": null, + "type": "smoothstep", + "data": {}, + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } + } + ], + "nodeCounter": 11, + "solverParams": { + "dt": "0.02", + "dt_min": "1e-6", + "dt_max": "1.0", + "Solver": "SSPRK22", + "tolerance_fpi": "1e-6", + "iterations_max": "100", + "log": "true", + "simulation_duration": "2", + "extra_params": "{}" + }, + "globalVariables": [] +} \ No newline at end of file From db5014f0217cb04c627a2d55212ac3b78a048320 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Thu, 31 Jul 2025 15:25:57 -0400 Subject: [PATCH 5/5] added surface area --- saved_graphs/festim_example.json | 87 ++++++++++++++++---------------- src/App.jsx | 2 +- src/custom_pathsim_blocks.py | 9 +++- 3 files changed, 52 insertions(+), 46 deletions(-) diff --git a/saved_graphs/festim_example.json b/saved_graphs/festim_example.json index c247870c..b6b0abde 100644 --- a/saved_graphs/festim_example.json +++ b/saved_graphs/festim_example.json @@ -1,27 +1,5 @@ { "nodes": [ - { - "id": "2", - "type": "wall", - "position": { - "x": 919.6228024037504, - "y": 398.2293552785672 - }, - "data": { - "label": "wall 2", - "thickness": "1", - "temperature": "300", - "D_0": "0.01", - "E_D": "0", - "n_vertices": "100" - }, - "measured": { - "width": 70, - "height": 200 - }, - "selected": false, - "dragging": false - }, { "id": "4", "type": "constant", @@ -54,7 +32,7 @@ "width": 120, "height": 140 }, - "selected": true, + "selected": false, "dragging": false }, { @@ -92,15 +70,38 @@ }, "selected": false, "dragging": false + }, + { + "id": "12", + "type": "wall", + "position": { + "x": 952, + "y": 417 + }, + "data": { + "label": "wall", + "thickness": "1", + "surface_area": "1", + "temperature": "300", + "D_0": "0.05", + "E_D": "0", + "n_vertices": "100" + }, + "measured": { + "width": 70, + "height": 200 + }, + "selected": true, + "dragging": false } ], "edges": [ { - "id": "e4-2-to_c_L", + "id": "e4-9", "source": "4", - "target": "2", + "target": "9", "sourceHandle": null, - "targetHandle": "c_L", + "targetHandle": null, "type": "smoothstep", "data": {}, "style": { @@ -115,10 +116,10 @@ } }, { - "id": "e2-5-from_flux_L", - "source": "2", - "target": "5", - "sourceHandle": "flux_L", + "id": "e10-9", + "source": "10", + "target": "9", + "sourceHandle": null, "targetHandle": null, "type": "smoothstep", "data": {}, @@ -134,11 +135,11 @@ } }, { - "id": "e4-9", - "source": "4", - "target": "9", + "id": "e10-12-to_c_0", + "source": "10", + "target": "12", "sourceHandle": null, - "targetHandle": null, + "targetHandle": "c_0", "type": "smoothstep", "data": {}, "style": { @@ -153,11 +154,11 @@ } }, { - "id": "e10-2-to_c_0", - "source": "10", - "target": "2", + "id": "e4-12-to_c_L", + "source": "4", + "target": "12", "sourceHandle": null, - "targetHandle": "c_0", + "targetHandle": "c_L", "type": "smoothstep", "data": {}, "style": { @@ -172,10 +173,10 @@ } }, { - "id": "e10-9", - "source": "10", - "target": "9", - "sourceHandle": null, + "id": "e12-5-from_flux_L", + "source": "12", + "target": "5", + "sourceHandle": "flux_L", "targetHandle": null, "type": "smoothstep", "data": {}, @@ -191,7 +192,7 @@ } } ], - "nodeCounter": 11, + "nodeCounter": 13, "solverParams": { "dt": "0.02", "dt_min": "1e-6", diff --git a/src/App.jsx b/src/App.jsx index 57468e8c..c57f4d9d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -609,7 +609,7 @@ export default function App() { nodeData = { ...nodeData, f1: '1/3', f2: '1/3', f3: '1/3' }; break; case 'wall': - nodeData = { ...nodeData, thickness: '', temperature: '', D_0: '1', E_D: '0', n_vertices: '100' }; + nodeData = { ...nodeData, thickness: '', surface_area: '1', temperature: '', D_0: '1', E_D: '0', n_vertices: '100' }; break; case 'bubbler': nodeData = { ...nodeData, conversion_efficiency: '0.95', vial_efficiency: '0.9', replacement_times: '' }; diff --git a/src/custom_pathsim_blocks.py b/src/custom_pathsim_blocks.py index d1dca5cd..e734f128 100644 --- a/src/custom_pathsim_blocks.py +++ b/src/custom_pathsim_blocks.py @@ -177,7 +177,9 @@ def create_reset_events(self) -> list[pathsim.blocks.Schedule]: class FestimWall(Block): - def __init__(self, thickness, temperature, D_0, E_D, n_vertices=100): + def __init__( + self, thickness, temperature, D_0, E_D, surface_area=1, n_vertices=100 + ): super().__init__() try: import festim as F @@ -186,6 +188,7 @@ def __init__(self, thickness, temperature, D_0, E_D, n_vertices=100): self.thickness = thickness self.temperature = temperature + self.surface_area = surface_area self.D_0 = D_0 self.E_D = E_D self.n_vertices = n_vertices @@ -262,5 +265,7 @@ def update(self, t): flux_0, flux_L = self.update_festim_model( c_0=c_0, c_L=c_L, stepsize=t - self.t ) - # error control + + flux_0 *= self.surface_area + flux_L *= self.surface_area return self.outputs.update_from_array([flux_0, flux_L])