Skip to content

Commit 697bb0c

Browse files
Merge pull request #69 from festim-dev/bubbler
Bubbler subsystem
2 parents 7d57959 + de3c78c commit 697bb0c

File tree

8 files changed

+604
-373
lines changed

8 files changed

+604
-373
lines changed

saved_graphs/baby.json

Lines changed: 271 additions & 371 deletions
Large diffs are not rendered by default.

src/App.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import DefaultNode from './DefaultNode';
2727
import { makeEdge } from './CustomEdge';
2828
import MultiplierNode from './MultiplierNode';
2929
import { Splitter2Node, Splitter3Node } from './Splitters';
30+
import BubblerNode from './BubblerNode';
3031

3132
// Add nodes as a node type for this script
3233
const nodeTypes = {
@@ -47,6 +48,7 @@ const nodeTypes = {
4748
pid: DefaultNode,
4849
splitter2: Splitter2Node,
4950
splitter3: Splitter3Node,
51+
bubbler: BubblerNode,
5052
};
5153

5254
// 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() {
467469
case 'splitter3':
468470
nodeData = { ...nodeData, f1: '1/3', f2: '1/3', f3: '1/3' };
469471
break;
472+
case 'bubbler':
473+
nodeData = { ...nodeData, conversion_efficiency: '0.95', vial_efficiency: '0.9', replacement_time: '' };
470474
default:
471475
// For any other types, just use basic data
472476
break;

src/BubblerNode.jsx

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import React from 'react';
2+
import { Handle } from '@xyflow/react';
3+
4+
export default function BubblerNode({ data }) {
5+
return (
6+
<div
7+
style={{
8+
width: 210,
9+
height: 140,
10+
background: '#DDE6ED',
11+
color: 'black',
12+
borderRadius: 8,
13+
padding: 10,
14+
fontWeight: 'bold',
15+
position: 'relative',
16+
cursor: 'pointer',
17+
}}
18+
>
19+
<div style={{ marginTop: '30%', textAlign: 'center' }}>{data.label}</div>
20+
21+
{/* Labels for sample in handles */}
22+
23+
<div style={{
24+
position: 'absolute',
25+
left: '2px',
26+
bottom: '20%',
27+
fontSize: '12px',
28+
fontWeight: 'bold',
29+
textAlign: 'right'
30+
}}>
31+
Sample in
32+
</div>
33+
<div style={{
34+
position: 'absolute',
35+
left: '6px',
36+
top: '29%',
37+
fontSize: '12px',
38+
fontWeight: 'normal',
39+
}}>
40+
soluble
41+
</div>
42+
<div style={{
43+
position: 'absolute',
44+
left: '6px',
45+
top: '62%',
46+
fontSize: '12px',
47+
fontWeight: 'normal',
48+
}}>
49+
insoluble
50+
</div>
51+
52+
<Handle type="target" id="sample_in_soluble" position="left" style={{ background: '#555', top: '33%' }} />
53+
<Handle type="target" id="sample_in_insoluble" position="left" style={{ background: '#555', top: '66%' }} />
54+
55+
<div style={{
56+
position: 'absolute',
57+
top: '6px',
58+
left: '6%',
59+
fontSize: '12px',
60+
fontWeight: 'normal',
61+
}}>
62+
Vials 1
63+
</div>
64+
<div style={{
65+
position: 'absolute',
66+
top: '6px',
67+
left: '38%',
68+
fontSize: '12px',
69+
fontWeight: 'normal',
70+
}}>2</div>
71+
72+
<div style={{
73+
position: 'absolute',
74+
top: '6px',
75+
left: '58%',
76+
fontSize: '12px',
77+
fontWeight: 'normal',
78+
}}>
79+
3
80+
</div>
81+
<div style={{
82+
position: 'absolute',
83+
top: '6px',
84+
left: '78%',
85+
fontSize: '12px',
86+
fontWeight: 'normal',
87+
}}>
88+
4
89+
</div>
90+
91+
<Handle type="source" id="vial1" position="top" style={{ background: '#555', left: '20%'}} />
92+
<Handle type="source" id="vial2" position="top" style={{ background: '#555', left: '40%' }} />
93+
<Handle type="source" id="vial3" position="top" style={{ background: '#555', left: '60%' }} />
94+
<Handle type="source" id="vial4" position="top" style={{ background: '#555', left: '80%' }} />
95+
96+
97+
<div style={{
98+
position: 'absolute',
99+
right: '6px',
100+
bottom: '50%',
101+
fontSize: '12px',
102+
fontWeight: 'normal',
103+
textAlign: 'right'
104+
}}>
105+
out
106+
</div>
107+
108+
<Handle type="source" id="sample_out" position="right" style={{ background: '#555' }} />
109+
</div>
110+
);
111+
}

src/backend.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import plotly
99
import json as plotly_json
1010

11-
1211
from .convert_to_python import convert_graph_to_python
1312
from .pathsim_utils import make_pathsim_model
1413
from pathsim.blocks import Scope

src/convert_to_python.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from .custom_pathsim_blocks import (
77
Process,
88
Splitter,
9+
Bubbler,
910
)
1011
from .pathsim_utils import (
1112
map_str_to_object,
@@ -146,6 +147,21 @@ def make_edge_data(data: dict) -> list[dict]:
146147
raise ValueError(
147148
f"Invalid source handle '{edge['sourceHandle']}' for {edge}."
148149
)
150+
elif isinstance(block, Bubbler):
151+
if edge["sourceHandle"] == "vial1":
152+
output_index = 0
153+
elif edge["sourceHandle"] == "vial2":
154+
output_index = 1
155+
elif edge["sourceHandle"] == "vial3":
156+
output_index = 2
157+
elif edge["sourceHandle"] == "vial4":
158+
output_index = 3
159+
elif edge["sourceHandle"] == "sample_out":
160+
output_index = 4
161+
else:
162+
raise ValueError(
163+
f"Invalid source handle '{edge['sourceHandle']}' for {edge}."
164+
)
149165
else:
150166
output_index = 0
151167

src/custom_pathsim_blocks.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from pathsim.blocks import Block, ODE
2+
import pathsim.blocks
3+
from pathsim import Subsystem, Interface, Connection
24
import numpy as np
35

46

@@ -35,3 +37,136 @@ def update(self, t):
3537
u = self.inputs[0]
3638
# mult by fractions and update outputs
3739
self.outputs.update_from_array(self.fractions * u)
40+
41+
42+
# BUBBLER SYSTEM
43+
44+
45+
class Bubbler(Subsystem):
46+
"""Subsystem representing a tritium bubbling system with 4 vials."""
47+
48+
vial_efficiency: float
49+
conversion_efficiency: float
50+
n_soluble_vials: float
51+
n_insoluble_vials: float
52+
53+
def __init__(
54+
self,
55+
conversion_efficiency=0.9,
56+
vial_efficiency=0.9,
57+
replacement_times=None,
58+
):
59+
"""
60+
Args:
61+
conversion_efficiency: Conversion efficiency from insoluble to soluble (between 0 and 1).
62+
vial_efficiency: collection efficiency of each vial (between 0 and 1).
63+
replacement_times: List of times at which each vial is replaced. If None, no replacement
64+
events are created. If a single value is provided, it is used for all vials.
65+
If a single list of floats is provided, it will be used for all vials.
66+
If a list of lists is provided, each sublist corresponds to the replacement times for each vial.
67+
"""
68+
self.reset_times = replacement_times
69+
self.n_soluble_vials = 2
70+
self.n_insoluble_vials = 2
71+
self.vial_efficiency = vial_efficiency
72+
col_eff1 = Splitter(n=2, fractions=[vial_efficiency, 1 - vial_efficiency])
73+
vial_1 = pathsim.blocks.Integrator()
74+
col_eff2 = Splitter(n=2, fractions=[vial_efficiency, 1 - vial_efficiency])
75+
vial_2 = pathsim.blocks.Integrator()
76+
77+
conversion_eff = Splitter(
78+
n=2, fractions=[conversion_efficiency, 1 - conversion_efficiency]
79+
)
80+
81+
col_eff3 = Splitter(n=2, fractions=[vial_efficiency, 1 - vial_efficiency])
82+
vial_3 = pathsim.blocks.Integrator()
83+
col_eff4 = Splitter(n=2, fractions=[vial_efficiency, 1 - vial_efficiency])
84+
vial_4 = pathsim.blocks.Integrator()
85+
86+
add1 = pathsim.blocks.Adder()
87+
add2 = pathsim.blocks.Adder()
88+
89+
interface = Interface()
90+
91+
self.vials = [vial_1, vial_2, vial_3, vial_4]
92+
93+
blocks = [
94+
vial_1,
95+
col_eff1,
96+
vial_2,
97+
col_eff2,
98+
conversion_eff,
99+
vial_3,
100+
col_eff3,
101+
vial_4,
102+
col_eff4,
103+
add1,
104+
add2,
105+
interface,
106+
]
107+
connections = [
108+
Connection(interface[0], col_eff1),
109+
Connection(col_eff1[0], vial_1),
110+
Connection(col_eff1[1], col_eff2),
111+
Connection(col_eff2[0], vial_2),
112+
Connection(col_eff2[1], conversion_eff),
113+
Connection(conversion_eff[0], add1[0]),
114+
Connection(conversion_eff[1], add2[0]),
115+
Connection(interface[1], add1[1]),
116+
Connection(add1, col_eff3),
117+
Connection(col_eff3[0], vial_3),
118+
Connection(col_eff3[1], col_eff4),
119+
Connection(col_eff4[0], vial_4),
120+
Connection(col_eff4[1], add2[1]),
121+
Connection(vial_1, interface[0]),
122+
Connection(vial_2, interface[1]),
123+
Connection(vial_3, interface[2]),
124+
Connection(vial_4, interface[3]),
125+
Connection(add2, interface[4]),
126+
]
127+
super().__init__(blocks, connections)
128+
129+
def _create_reset_events_one_vial(
130+
self, block, reset_times
131+
) -> list[pathsim.blocks.Schedule]:
132+
events = []
133+
134+
def reset_itg(_):
135+
block.reset()
136+
137+
for t in reset_times:
138+
events.append(
139+
pathsim.blocks.Schedule(t_start=t, t_end=t, func_act=reset_itg)
140+
)
141+
return events
142+
143+
def create_reset_events(self) -> list[pathsim.blocks.Schedule]:
144+
"""Create reset events for all vials based on the replacement times.
145+
146+
Raises:
147+
ValueError: If reset_times is not valid.
148+
149+
Returns:
150+
list of reset events.
151+
"""
152+
reset_times = self.reset_times
153+
events = []
154+
# if reset_times is a single list use it for all vials
155+
if reset_times is None:
156+
return events
157+
if isinstance(reset_times, (int, float)):
158+
reset_times = [reset_times]
159+
# if it's a flat list use it for all vials
160+
elif isinstance(reset_times, list) and all(
161+
isinstance(t, (int, float)) for t in reset_times
162+
):
163+
reset_times = [reset_times] * len(self.vials)
164+
elif isinstance(reset_times, np.ndarray) and reset_times.ndim == 1:
165+
reset_times = [reset_times.tolist()] * len(self.vials)
166+
elif isinstance(reset_times, list) and len(reset_times) != len(self.vials):
167+
raise ValueError(
168+
"reset_times must be a single value or a list with the same length as the number of vials"
169+
)
170+
for i, vial in enumerate(self.vials):
171+
events.extend(self._create_reset_events_one_vial(vial, reset_times[i]))
172+
return events

src/pathsim_utils.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
PID,
2020
Schedule,
2121
)
22-
from .custom_pathsim_blocks import Process, Splitter
22+
from .custom_pathsim_blocks import Process, Splitter, Bubbler
2323
from flask import jsonify
2424

2525
NAME_TO_SOLVER = {
@@ -46,6 +46,7 @@
4646
"integrator": Integrator,
4747
"function": Function,
4848
"delay": Delay,
49+
"bubbler": Bubbler,
4950
}
5051

5152

@@ -135,6 +136,24 @@ def func(x):
135136
return block
136137

137138

139+
def create_bubbler(node: dict) -> Bubbler:
140+
"""
141+
Create a Bubbler block based on the node data.
142+
"""
143+
# Extract parameters from node data
144+
block = Bubbler(
145+
conversion_efficiency=eval(node["data"]["conversion_efficiency"]),
146+
vial_efficiency=eval(node["data"]["vial_efficiency"]),
147+
replacement_times=eval(node["data"]["replacement_time"])
148+
if node["data"].get("replacement_time") != ""
149+
else None,
150+
)
151+
152+
events = block.create_reset_events()
153+
154+
return block, events
155+
156+
138157
def create_scope(node: dict, edges, nodes) -> Scope:
139158
# Find all incoming edges to this node and sort by source id for consistent ordering
140159
incoming_edges = [edge for edge in edges if edge["target"] == node["id"]]
@@ -326,6 +345,9 @@ def make_blocks(
326345
eval(node["data"]["f3"], eval_namespace),
327346
],
328347
)
348+
elif block_type == "bubbler":
349+
block, events_bubbler = create_bubbler(node)
350+
events.extend(events_bubbler)
329351
else: # try automated construction
330352
block = auto_block_construction(node, eval_namespace)
331353

@@ -373,6 +395,21 @@ def make_connections(nodes, edges, blocks) -> list[Connection]:
373395
raise ValueError(
374396
f"Invalid source handle '{edge['sourceHandle']}' for {edge}."
375397
)
398+
elif isinstance(block, Bubbler):
399+
if edge["sourceHandle"] == "vial1":
400+
output_index = 0
401+
elif edge["sourceHandle"] == "vial2":
402+
output_index = 1
403+
elif edge["sourceHandle"] == "vial3":
404+
output_index = 2
405+
elif edge["sourceHandle"] == "vial4":
406+
output_index = 3
407+
elif edge["sourceHandle"] == "sample_out":
408+
output_index = 4
409+
else:
410+
raise ValueError(
411+
f"Invalid source handle '{edge['sourceHandle']}' for {edge}."
412+
)
376413
else:
377414
output_index = 0
378415

0 commit comments

Comments
 (0)