diff --git a/saved_graphs/baby.json b/saved_graphs/baby.json index 91897bc2..bd3b72df 100644 --- a/saved_graphs/baby.json +++ b/saved_graphs/baby.json @@ -3,7 +3,7 @@ { "data": { "amplitude": "1", - "delay": "1", + "delay": "2", "label": "stepsource 2" }, "dragging": false, @@ -13,10 +13,10 @@ "width": 120 }, "position": { - "x": 339, - "y": 435 + "x": 398.37785598478024, + "y": 489.09982434168865 }, - "selected": false, + "selected": true, "type": "stepsource" }, { @@ -41,7 +41,7 @@ { "data": { "gain": "-1", - "label": "amplifier 4" + "label": "x (-1)" }, "dragging": false, "id": "4", @@ -67,7 +67,7 @@ "width": 120 }, "position": { - "x": 875, + "x": 859.1659050707252, "y": 126 }, "selected": false, @@ -111,8 +111,8 @@ { "data": { "initial_value": "", - "label": "process 8", - "residence_time": "10", + "label": "BABY", + "residence_time": "(k_IV + k_OV)/baby_vol", "source_term": "" }, "dragging": false, @@ -122,224 +122,135 @@ "width": 200 }, "position": { - "x": 1102, - "y": 571 + "x": 1020.1905095320806, + "y": 573.6390158215457 }, "selected": false, "type": "process" }, { "data": { - "f1": "0.9", - "f2": "0.1", - "label": "Collection eff." + "label": "IV vial activity" }, "dragging": false, - "id": "11", - "measured": { - "height": 120, - "width": 120 - }, - "position": { - "x": 1305, - "y": 698 - }, - "selected": false, - "type": "splitter2" - }, - { - "data": { - "f1": "0.9", - "f2": "0.1", - "label": "Collection eff." - }, - "dragging": false, - "id": "13", + "id": "21", "measured": { - "height": 120, + "height": 140, "width": 120 }, "position": { - "x": 1312, - "y": 871 + "x": 1949.5229665105167, + "y": 563.941606900507 }, "selected": false, - "type": "splitter2" + "type": "scope" }, { "data": { - "f1": "0.99", - "f2": "0.01", - "label": "HT -> HTO eff." + "f1": "0.01", + "f2": "0.99", + "label": "soluble vs insoluble" }, "dragging": false, - "id": "14", + "id": "1", "measured": { "height": 120, "width": 120 }, "position": { - "x": 1353.9258928782547, - "y": 1099.0441878253396 + "x": 1394.0782538373574, + "y": 743.9802025169162 }, "selected": false, "type": "splitter2" }, { - "data": { - "f1": "0.9", - "f2": "0.1", - "label": "Collection eff." - }, - "dragging": false, - "id": "16", - "measured": { - "height": 120, - "width": 120 - }, + "id": "23", + "type": "bubbler", "position": { - "x": 1659, - "y": 771 + "x": 1595, + "y": 722 }, - "selected": false, - "type": "splitter2" - }, - { "data": { - "f1": "0.9", - "f2": "0.1", - "label": "Collection eff." + "label": "IV bubbler", + "conversion_efficiency": "0.95", + "vial_efficiency": "0.9", + "replacement_time": "np.arange(5, 50, step=3)" }, - "dragging": false, - "id": "18", "measured": { - "height": 120, - "width": 120 - }, - "position": { - "x": 1667, - "y": 951 + "width": 230, + "height": 160 }, "selected": false, - "type": "splitter2" - }, - { - "data": { - "initial_value": "", - "label": "ambient air", - "reset_times": "" - }, - "dragging": false, - "id": "19", - "measured": { - "height": 48, - "width": 200 - }, - "position": { - "x": 2040, - "y": 913 - }, - "selected": false, - "type": "integrator" + "dragging": false }, { - "data": { - "label": "adder 20" - }, - "dragging": false, - "id": "20", - "measured": { - "height": 64, - "width": 64 - }, + "id": "24", + "type": "splitter2", "position": { - "x": 1769, - "y": 1185 + "x": 1193, + "y": 851 }, - "selected": false, - "type": "adder" - }, - { "data": { - "label": "Inventories" + "label": "IV vs OV", + "f1": "k_IV/(k_IV + k_OV)", + "f2": "k_OV/(k_IV + k_OV)" }, - "dragging": false, - "id": "21", "measured": { - "height": 140, - "width": 120 - }, - "position": { - "x": 1822.4225842692827, - "y": 260.99001087345584 + "width": 120, + "height": 120 }, "selected": false, - "type": "scope" + "dragging": false }, { - "data": { - "label": "adder 0" - }, - "dragging": false, - "id": "0", - "measured": { - "height": 64, - "width": 64 - }, + "id": "25", + "type": "splitter2", "position": { - "x": 1517.8462568023597, - "y": 978.8349758222805 + "x": 1410.747877541758, + "y": 965.2139451372932 }, - "selected": false, - "type": "adder" - }, - { "data": { + "label": "soluble vs insoluble", "f1": "0.01", - "f2": "0.99", - "label": "soluble vs insoluble" + "f2": "0.99" }, - "dragging": false, - "id": "1", "measured": { - "height": 120, - "width": 120 - }, - "position": { - "x": 865.0782538373575, - "y": 719.9802025169162 + "width": 120, + "height": 120 }, "selected": false, - "type": "splitter2" + "dragging": false }, { - "id": "24", - "type": "integrator", + "id": "26", + "type": "bubbler", "position": { - "x": 1455, - "y": 316 + "x": 1602.4670834180595, + "y": 970.2206053879095 }, "data": { - "label": "IV 1", - "initial_value": "", - "reset_times": "" + "label": "OV bubbler", + "conversion_efficiency": "0.95", + "vial_efficiency": "0.9", + "replacement_time": "np.arange(5, 50, step=5)" }, "measured": { - "width": 200, - "height": 48 + "width": 230, + "height": 160 }, "selected": false, "dragging": false }, { - "id": "25", + "id": "27", "type": "integrator", "position": { - "x": 1525, - "y": 416 + "x": 2059.579367811878, + "y": 919.8031067000171 }, "data": { - "label": "IV 2", + "label": "environment", "initial_value": "", "reset_times": "" }, @@ -351,39 +262,35 @@ "dragging": false }, { - "id": "26", - "type": "integrator", + "id": "28", + "type": "scope", "position": { - "x": 1532, - "y": 505 + "x": 1923.4902343023584, + "y": 1097.1334152526842 }, "data": { - "label": "IV 3", - "initial_value": "", - "reset_times": "" + "label": "OV vial activity" }, "measured": { - "width": 200, - "height": 48 + "width": 120, + "height": 140 }, "selected": false, "dragging": false }, { - "id": "27", - "type": "integrator", + "id": "30", + "type": "scope", "position": { - "x": 1640, - "y": 572 + "x": 916, + "y": 786 }, "data": { - "label": "IV 4", - "initial_value": "", - "reset_times": "" + "label": "BABY inventory" }, "measured": { - "width": 200, - "height": 48 + "width": 120, + "height": 140 }, "selected": false, "dragging": false @@ -506,220 +413,201 @@ "type": "smoothstep" }, { + "id": "e24-1-from_source1", + "source": "24", + "target": "1", + "sourceHandle": "source1", + "targetHandle": null, + "type": "smoothstep", "data": {}, - "id": "e11-13-from_source2", - "markerEnd": { - "color": "#ECDFCC", - "height": 20, - "type": "arrowclosed", - "width": 20 - }, - "source": "11", - "sourceHandle": "source2", "style": { - "stroke": "#ECDFCC", - "strokeWidth": 2 + "strokeWidth": 2, + "stroke": "#ECDFCC" }, - "target": "13", - "targetHandle": null, - "type": "smoothstep" - }, - { - "data": {}, - "id": "e13-14-from_source2", "markerEnd": { - "color": "#ECDFCC", - "height": 20, "type": "arrowclosed", - "width": 20 - }, - "source": "13", - "sourceHandle": "source2", - "style": { - "stroke": "#ECDFCC", - "strokeWidth": 2 - }, - "target": "14", - "targetHandle": null, - "type": "smoothstep" + "width": 20, + "height": 20, + "color": "#ECDFCC" + } }, { + "id": "e1-23-from_source1-to_sample_in_soluble", + "source": "1", + "target": "23", + "sourceHandle": "source1", + "targetHandle": "sample_in_soluble", + "type": "smoothstep", "data": {}, - "id": "e16-18-from_source2", + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, "markerEnd": { - "color": "#ECDFCC", - "height": 20, "type": "arrowclosed", - "width": 20 - }, - "source": "16", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } + }, + { + "id": "e1-23-from_source2-to_sample_in_insoluble", + "source": "1", + "target": "23", "sourceHandle": "source2", + "targetHandle": "sample_in_insoluble", + "type": "smoothstep", + "data": {}, "style": { - "stroke": "#ECDFCC", - "strokeWidth": 2 + "strokeWidth": 2, + "stroke": "#ECDFCC" }, - "target": "18", - "targetHandle": null, - "type": "smoothstep" + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } }, { + "id": "e23-21-from_vial1", + "source": "23", + "target": "21", + "sourceHandle": "vial1", + "targetHandle": null, + "type": "smoothstep", "data": {}, - "id": "e14-20-from_source2", - "markerEnd": { - "color": "#ECDFCC", - "height": 20, - "type": "arrowclosed", - "width": 20 - }, - "source": "14", - "sourceHandle": "source2", "style": { - "stroke": "#ECDFCC", - "strokeWidth": 2 + "strokeWidth": 2, + "stroke": "#ECDFCC" }, - "target": "20", - "targetHandle": null, - "type": "smoothstep" + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } }, { + "id": "e23-21-from_vial2", + "source": "23", + "target": "21", + "sourceHandle": "vial2", + "targetHandle": null, + "type": "smoothstep", "data": {}, - "id": "e18-20-from_source2", - "markerEnd": { - "color": "#ECDFCC", - "height": 20, - "type": "arrowclosed", - "width": 20 - }, - "source": "18", - "sourceHandle": "source2", "style": { - "stroke": "#ECDFCC", - "strokeWidth": 2 + "strokeWidth": 2, + "stroke": "#ECDFCC" }, - "target": "20", - "targetHandle": null, - "type": "smoothstep" + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } }, { + "id": "e23-21-from_vial3", + "source": "23", + "target": "21", + "sourceHandle": "vial3", + "targetHandle": null, + "type": "smoothstep", "data": {}, - "id": "e20-19", - "markerEnd": { - "color": "#ECDFCC", - "height": 20, - "type": "arrowclosed", - "width": 20 - }, - "source": "20", - "sourceHandle": null, "style": { - "stroke": "#ECDFCC", - "strokeWidth": 2 + "strokeWidth": 2, + "stroke": "#ECDFCC" }, - "target": "19", - "targetHandle": null, - "type": "smoothstep" + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } }, { + "id": "e23-21-from_vial4", + "source": "23", + "target": "21", + "sourceHandle": "vial4", + "targetHandle": null, + "type": "smoothstep", "data": {}, - "id": "e8-1-from_mass_flow_rate", + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, "markerEnd": { - "color": "#ECDFCC", - "height": 20, "type": "arrowclosed", - "width": 20 - }, + "width": 20, + "height": 20, + "color": "#ECDFCC" + } + }, + { + "id": "e8-24-from_mass_flow_rate", "source": "8", + "target": "24", "sourceHandle": "mass_flow_rate", - "style": { - "stroke": "#ECDFCC", - "strokeWidth": 2 - }, - "target": "1", "targetHandle": null, - "type": "smoothstep" - }, - { + "type": "smoothstep", "data": {}, - "id": "e1-11-from_source1", - "markerEnd": { - "color": "#ECDFCC", - "height": 20, - "type": "arrowclosed", - "width": 20 - }, - "source": "1", - "sourceHandle": "source1", "style": { - "stroke": "#ECDFCC", - "strokeWidth": 2 + "strokeWidth": 2, + "stroke": "#ECDFCC" }, - "target": "11", - "targetHandle": null, - "type": "smoothstep" - }, - { - "data": {}, - "id": "e1-0-from_source2", "markerEnd": { - "color": "#ECDFCC", - "height": 20, "type": "arrowclosed", - "width": 20 - }, - "source": "1", - "sourceHandle": "source2", - "style": { - "stroke": "#ECDFCC", - "strokeWidth": 2 - }, - "target": "0", - "targetHandle": null, - "type": "smoothstep" + "width": 20, + "height": 20, + "color": "#ECDFCC" + } }, { + "id": "e24-25-from_source2", + "source": "24", + "target": "25", + "sourceHandle": "source2", + "targetHandle": null, + "type": "smoothstep", "data": {}, - "id": "e0-16", - "markerEnd": { - "color": "#ECDFCC", - "height": 20, - "type": "arrowclosed", - "width": 20 - }, - "source": "0", - "sourceHandle": null, "style": { - "stroke": "#ECDFCC", - "strokeWidth": 2 + "strokeWidth": 2, + "stroke": "#ECDFCC" }, - "target": "16", - "targetHandle": null, - "type": "smoothstep" - }, - { - "data": {}, - "id": "e14-0-from_source1", "markerEnd": { - "color": "#ECDFCC", - "height": 20, "type": "arrowclosed", - "width": 20 - }, - "source": "14", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } + }, + { + "id": "e25-26-from_source1-to_sample_in_soluble", + "source": "25", + "target": "26", "sourceHandle": "source1", + "targetHandle": "sample_in_soluble", + "type": "smoothstep", + "data": {}, "style": { - "stroke": "#ECDFCC", - "strokeWidth": 2 + "strokeWidth": 2, + "stroke": "#ECDFCC" }, - "target": "0", - "targetHandle": null, - "type": "smoothstep" + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } }, { - "id": "e24-21", - "source": "24", - "target": "21", - "sourceHandle": null, - "targetHandle": null, + "id": "e25-26-from_source2-to_sample_in_insoluble", + "source": "25", + "target": "26", + "sourceHandle": "source2", + "targetHandle": "sample_in_insoluble", "type": "smoothstep", "data": {}, "style": { @@ -734,10 +622,10 @@ } }, { - "id": "e25-21", - "source": "25", - "target": "21", - "sourceHandle": null, + "id": "e23-27-from_sample_out", + "source": "23", + "target": "27", + "sourceHandle": "sample_out", "targetHandle": null, "type": "smoothstep", "data": {}, @@ -753,10 +641,10 @@ } }, { - "id": "e26-21", + "id": "e26-27-from_sample_out", "source": "26", - "target": "21", - "sourceHandle": null, + "target": "27", + "sourceHandle": "sample_out", "targetHandle": null, "type": "smoothstep", "data": {}, @@ -772,10 +660,10 @@ } }, { - "id": "e27-21", - "source": "27", - "target": "21", - "sourceHandle": null, + "id": "e26-28-from_vial1", + "source": "26", + "target": "28", + "sourceHandle": "vial1", "targetHandle": null, "type": "smoothstep", "data": {}, @@ -791,10 +679,10 @@ } }, { - "id": "e11-24-from_source1", - "source": "11", - "target": "24", - "sourceHandle": "source1", + "id": "e26-28-from_vial2", + "source": "26", + "target": "28", + "sourceHandle": "vial2", "targetHandle": null, "type": "smoothstep", "data": {}, @@ -810,10 +698,10 @@ } }, { - "id": "e13-25-from_source1", - "source": "13", - "target": "25", - "sourceHandle": "source1", + "id": "e26-28-from_vial3", + "source": "26", + "target": "28", + "sourceHandle": "vial3", "targetHandle": null, "type": "smoothstep", "data": {}, @@ -829,10 +717,10 @@ } }, { - "id": "e16-26-from_source1", - "source": "16", - "target": "26", - "sourceHandle": "source1", + "id": "e26-28-from_vial4", + "source": "26", + "target": "28", + "sourceHandle": "vial4", "targetHandle": null, "type": "smoothstep", "data": {}, @@ -848,10 +736,10 @@ } }, { - "id": "e18-27-from_source1", - "source": "18", - "target": "27", - "sourceHandle": "source1", + "id": "e8-30-from_inv", + "source": "8", + "target": "30", + "sourceHandle": "inv", "targetHandle": null, "type": "smoothstep", "data": {}, @@ -867,24 +755,36 @@ } } ], - "nodeCounter": 28, + "nodeCounter": 31, "solverParams": { - "Solver": "SSPRK22", "dt": "0.01", - "dt_max": "1.0", "dt_min": "1e-6", - "extra_params": "{}", + "dt_max": "1.0", + "Solver": "SSPRK22", + "tolerance_fpi": "1e-6", "iterations_max": "100", "log": "true", - "simulation_duration": "duration", - "tolerance_fpi": "1e-6" + "simulation_duration": "50.0", + "extra_params": "{\"tolerance_lte_rel\":1e-4, \"tolerance_lte_abs\":1e-9}" }, "globalVariables": [ { - "id": "1753728685046", - "name": "duration", - "nameError": false, - "value": "50" + "id": "1753792090540", + "name": "baby_vol", + "value": "1", + "nameError": false + }, + { + "id": "1753797344108", + "name": "k_IV", + "value": "5", + "nameError": false + }, + { + "id": "1753797380350", + "name": "k_OV", + "value": "1", + "nameError": false } ] } \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index cdb2ef19..6f657110 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -27,6 +27,7 @@ import DefaultNode from './DefaultNode'; import { makeEdge } from './CustomEdge'; import MultiplierNode from './MultiplierNode'; import { Splitter2Node, Splitter3Node } from './Splitters'; +import BubblerNode from './BubblerNode'; // Add nodes as a node type for this script const nodeTypes = { @@ -47,6 +48,7 @@ const nodeTypes = { pid: DefaultNode, splitter2: Splitter2Node, splitter3: Splitter3Node, + bubbler: BubblerNode, }; // Defining initial nodes and edges. In the data section, we have label, but also parameters specific to the node. @@ -467,6 +469,8 @@ export default function App() { case 'splitter3': nodeData = { ...nodeData, f1: '1/3', f2: '1/3', f3: '1/3' }; break; + case 'bubbler': + nodeData = { ...nodeData, conversion_efficiency: '0.95', vial_efficiency: '0.9', replacement_time: '' }; default: // For any other types, just use basic data break; diff --git a/src/BubblerNode.jsx b/src/BubblerNode.jsx new file mode 100644 index 00000000..21e5a656 --- /dev/null +++ b/src/BubblerNode.jsx @@ -0,0 +1,111 @@ +import React from 'react'; +import { Handle } from '@xyflow/react'; + +export default function BubblerNode({ data }) { + return ( +
+
{data.label}
+ + {/* Labels for sample in handles */} + +
+ Sample in +
+
+ soluble +
+
+ insoluble +
+ + + + +
+ Vials 1 +
+
2
+ +
+ 3 +
+
+ 4 +
+ + + + + + + +
+ out +
+ + +
+ ); +} diff --git a/src/backend.py b/src/backend.py index a35dc9fd..f5e5db83 100644 --- a/src/backend.py +++ b/src/backend.py @@ -8,7 +8,6 @@ import plotly import json as plotly_json - from .convert_to_python import convert_graph_to_python from .pathsim_utils import make_pathsim_model from pathsim.blocks import Scope diff --git a/src/convert_to_python.py b/src/convert_to_python.py index 9c8561d8..29b45b75 100644 --- a/src/convert_to_python.py +++ b/src/convert_to_python.py @@ -6,6 +6,7 @@ from .custom_pathsim_blocks import ( Process, Splitter, + Bubbler, ) from .pathsim_utils import ( map_str_to_object, @@ -146,6 +147,21 @@ def make_edge_data(data: dict) -> list[dict]: raise ValueError( f"Invalid source handle '{edge['sourceHandle']}' for {edge}." ) + elif isinstance(block, Bubbler): + if edge["sourceHandle"] == "vial1": + output_index = 0 + elif edge["sourceHandle"] == "vial2": + output_index = 1 + elif edge["sourceHandle"] == "vial3": + output_index = 2 + elif edge["sourceHandle"] == "vial4": + output_index = 3 + elif edge["sourceHandle"] == "sample_out": + output_index = 4 + else: + raise ValueError( + f"Invalid source handle '{edge['sourceHandle']}' for {edge}." + ) else: output_index = 0 diff --git a/src/custom_pathsim_blocks.py b/src/custom_pathsim_blocks.py index a3f9c7fe..a7784c08 100644 --- a/src/custom_pathsim_blocks.py +++ b/src/custom_pathsim_blocks.py @@ -1,4 +1,6 @@ from pathsim.blocks import Block, ODE +import pathsim.blocks +from pathsim import Subsystem, Interface, Connection import numpy as np @@ -35,3 +37,136 @@ def update(self, t): u = self.inputs[0] # mult by fractions and update outputs self.outputs.update_from_array(self.fractions * u) + + +# BUBBLER SYSTEM + + +class Bubbler(Subsystem): + """Subsystem representing a tritium bubbling system with 4 vials.""" + + vial_efficiency: float + conversion_efficiency: float + n_soluble_vials: float + n_insoluble_vials: float + + def __init__( + self, + conversion_efficiency=0.9, + vial_efficiency=0.9, + replacement_times=None, + ): + """ + Args: + conversion_efficiency: Conversion efficiency from insoluble to soluble (between 0 and 1). + vial_efficiency: collection efficiency of each vial (between 0 and 1). + replacement_times: List of times at which each vial is replaced. If None, no replacement + events are created. If a single value is provided, it is used for all vials. + If a single list of floats is provided, it will be used for all vials. + If a list of lists is provided, each sublist corresponds to the replacement times for each vial. + """ + self.reset_times = replacement_times + self.n_soluble_vials = 2 + self.n_insoluble_vials = 2 + self.vial_efficiency = vial_efficiency + col_eff1 = Splitter(n=2, fractions=[vial_efficiency, 1 - vial_efficiency]) + vial_1 = pathsim.blocks.Integrator() + col_eff2 = Splitter(n=2, fractions=[vial_efficiency, 1 - vial_efficiency]) + vial_2 = pathsim.blocks.Integrator() + + conversion_eff = Splitter( + n=2, fractions=[conversion_efficiency, 1 - conversion_efficiency] + ) + + col_eff3 = Splitter(n=2, fractions=[vial_efficiency, 1 - vial_efficiency]) + vial_3 = pathsim.blocks.Integrator() + col_eff4 = Splitter(n=2, fractions=[vial_efficiency, 1 - vial_efficiency]) + vial_4 = pathsim.blocks.Integrator() + + add1 = pathsim.blocks.Adder() + add2 = pathsim.blocks.Adder() + + interface = Interface() + + self.vials = [vial_1, vial_2, vial_3, vial_4] + + blocks = [ + vial_1, + col_eff1, + vial_2, + col_eff2, + conversion_eff, + vial_3, + col_eff3, + vial_4, + col_eff4, + add1, + add2, + interface, + ] + connections = [ + Connection(interface[0], col_eff1), + Connection(col_eff1[0], vial_1), + Connection(col_eff1[1], col_eff2), + Connection(col_eff2[0], vial_2), + Connection(col_eff2[1], conversion_eff), + Connection(conversion_eff[0], add1[0]), + Connection(conversion_eff[1], add2[0]), + Connection(interface[1], add1[1]), + Connection(add1, col_eff3), + Connection(col_eff3[0], vial_3), + Connection(col_eff3[1], col_eff4), + Connection(col_eff4[0], vial_4), + Connection(col_eff4[1], add2[1]), + Connection(vial_1, interface[0]), + Connection(vial_2, interface[1]), + Connection(vial_3, interface[2]), + Connection(vial_4, interface[3]), + Connection(add2, interface[4]), + ] + super().__init__(blocks, connections) + + def _create_reset_events_one_vial( + self, block, reset_times + ) -> list[pathsim.blocks.Schedule]: + events = [] + + def reset_itg(_): + block.reset() + + for t in reset_times: + events.append( + pathsim.blocks.Schedule(t_start=t, t_end=t, func_act=reset_itg) + ) + return events + + def create_reset_events(self) -> list[pathsim.blocks.Schedule]: + """Create reset events for all vials based on the replacement times. + + Raises: + ValueError: If reset_times is not valid. + + Returns: + list of reset events. + """ + reset_times = self.reset_times + events = [] + # if reset_times is a single list use it for all vials + if reset_times is None: + return events + if isinstance(reset_times, (int, float)): + reset_times = [reset_times] + # if it's a flat list use it for all vials + elif isinstance(reset_times, list) and all( + isinstance(t, (int, float)) for t in reset_times + ): + reset_times = [reset_times] * len(self.vials) + elif isinstance(reset_times, np.ndarray) and reset_times.ndim == 1: + reset_times = [reset_times.tolist()] * len(self.vials) + elif isinstance(reset_times, list) and len(reset_times) != len(self.vials): + raise ValueError( + "reset_times must be a single value or a list with the same length as the number of vials" + ) + for i, vial in enumerate(self.vials): + events.extend(self._create_reset_events_one_vial(vial, reset_times[i])) + return events diff --git a/src/pathsim_utils.py b/src/pathsim_utils.py index 8218d3fa..c40bc46b 100644 --- a/src/pathsim_utils.py +++ b/src/pathsim_utils.py @@ -19,7 +19,7 @@ PID, Schedule, ) -from .custom_pathsim_blocks import Process, Splitter +from .custom_pathsim_blocks import Process, Splitter, Bubbler from flask import jsonify NAME_TO_SOLVER = { @@ -46,6 +46,7 @@ "integrator": Integrator, "function": Function, "delay": Delay, + "bubbler": Bubbler, } @@ -135,6 +136,24 @@ def func(x): return block +def create_bubbler(node: dict) -> Bubbler: + """ + Create a Bubbler block based on the node data. + """ + # Extract parameters from node data + block = Bubbler( + conversion_efficiency=eval(node["data"]["conversion_efficiency"]), + vial_efficiency=eval(node["data"]["vial_efficiency"]), + replacement_times=eval(node["data"]["replacement_time"]) + if node["data"].get("replacement_time") != "" + else None, + ) + + events = block.create_reset_events() + + return block, events + + def create_scope(node: dict, edges, nodes) -> Scope: # Find all incoming edges to this node and sort by source id for consistent ordering incoming_edges = [edge for edge in edges if edge["target"] == node["id"]] @@ -326,6 +345,9 @@ def make_blocks( eval(node["data"]["f3"], eval_namespace), ], ) + elif block_type == "bubbler": + block, events_bubbler = create_bubbler(node) + events.extend(events_bubbler) else: # try automated construction block = auto_block_construction(node, eval_namespace) @@ -373,6 +395,21 @@ def make_connections(nodes, edges, blocks) -> list[Connection]: raise ValueError( f"Invalid source handle '{edge['sourceHandle']}' for {edge}." ) + elif isinstance(block, Bubbler): + if edge["sourceHandle"] == "vial1": + output_index = 0 + elif edge["sourceHandle"] == "vial2": + output_index = 1 + elif edge["sourceHandle"] == "vial3": + output_index = 2 + elif edge["sourceHandle"] == "vial4": + output_index = 3 + elif edge["sourceHandle"] == "sample_out": + output_index = 4 + else: + raise ValueError( + f"Invalid source handle '{edge['sourceHandle']}' for {edge}." + ) else: output_index = 0 diff --git a/test/test_custom_blocks.py b/test/test_custom_blocks.py new file mode 100644 index 00000000..08083b75 --- /dev/null +++ b/test/test_custom_blocks.py @@ -0,0 +1,29 @@ +import pathsim.blocks +from pathsim import Simulation, Connection +from src.custom_pathsim_blocks import Bubbler + + +def test_bubbler(): + my_bubbler = Bubbler() + + source_soluble = pathsim.blocks.Constant(1) + source_insoluble = pathsim.blocks.Constant(0.5) + environment = pathsim.blocks.Integrator() + sco = pathsim.blocks.Scope( + labels=["Vial 1", "Vial 2", "Vial 3", "Vial 4", "Environment"], + ) + + blocks = [source_soluble, source_insoluble, my_bubbler, environment, sco] + + connections = [ + Connection(source_soluble, my_bubbler[0]), + Connection(source_insoluble, my_bubbler[1]), + Connection(my_bubbler[0], sco[0]), + Connection(my_bubbler[1], sco[1]), + Connection(my_bubbler[2], sco[2]), + Connection(my_bubbler[3], sco[3]), + Connection(my_bubbler[4], environment), + ] + + sim = Simulation(blocks, connections) + sim.run(20)