Skip to content
642 changes: 271 additions & 371 deletions saved_graphs/baby.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
Background,
useNodesState,
useEdgesState,
addEdge,

Check failure on line 9 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

'addEdge' is defined but never used. Allowed unused vars must match /^[A-Z_]/u

Check failure on line 9 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

'addEdge' is defined but never used. Allowed unused vars must match /^[A-Z_]/u
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import './App.css';
Expand All @@ -27,6 +27,7 @@
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 = {
Expand All @@ -47,6 +48,7 @@
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.
Expand Down Expand Up @@ -467,7 +469,9 @@
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:

Check failure on line 474 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

Expected a 'break' statement before 'default'

Check failure on line 474 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

Expected a 'break' statement before 'default'
// For any other types, just use basic data
break;
}
Expand Down Expand Up @@ -535,7 +539,7 @@
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [selectedEdge, selectedNode]);

Check warning on line 542 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

React Hook useEffect has missing dependencies: 'deleteSelectedEdge' and 'deleteSelectedNode'. Either include them or remove the dependency array

Check warning on line 542 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

React Hook useEffect has missing dependencies: 'deleteSelectedEdge' and 'deleteSelectedNode'. Either include them or remove the dependency array

return (
<div style={{ width: '100vw', height: '100vh', position: 'relative' }}>
Expand Down
111 changes: 111 additions & 0 deletions src/BubblerNode.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React from 'react';
import { Handle } from '@xyflow/react';

export default function BubblerNode({ data }) {
return (
<div
style={{
width: 210,
height: 140,
background: '#DDE6ED',
color: 'black',
borderRadius: 8,
padding: 10,
fontWeight: 'bold',
position: 'relative',
cursor: 'pointer',
}}
>
<div style={{ marginTop: '30%', textAlign: 'center' }}>{data.label}</div>

{/* Labels for sample in handles */}

<div style={{
position: 'absolute',
left: '2px',
bottom: '20%',
fontSize: '12px',
fontWeight: 'bold',
textAlign: 'right'
}}>
Sample in
</div>
<div style={{
position: 'absolute',
left: '6px',
top: '29%',
fontSize: '12px',
fontWeight: 'normal',
}}>
soluble
</div>
<div style={{
position: 'absolute',
left: '6px',
top: '62%',
fontSize: '12px',
fontWeight: 'normal',
}}>
insoluble
</div>

<Handle type="target" id="sample_in_soluble" position="left" style={{ background: '#555', top: '33%' }} />
<Handle type="target" id="sample_in_insoluble" position="left" style={{ background: '#555', top: '66%' }} />

<div style={{
position: 'absolute',
top: '6px',
left: '6%',
fontSize: '12px',
fontWeight: 'normal',
}}>
Vials 1
</div>
<div style={{
position: 'absolute',
top: '6px',
left: '38%',
fontSize: '12px',
fontWeight: 'normal',
}}>2</div>

<div style={{
position: 'absolute',
top: '6px',
left: '58%',
fontSize: '12px',
fontWeight: 'normal',
}}>
3
</div>
<div style={{
position: 'absolute',
top: '6px',
left: '78%',
fontSize: '12px',
fontWeight: 'normal',
}}>
4
</div>

<Handle type="source" id="vial1" position="top" style={{ background: '#555', left: '20%'}} />
<Handle type="source" id="vial2" position="top" style={{ background: '#555', left: '40%' }} />
<Handle type="source" id="vial3" position="top" style={{ background: '#555', left: '60%' }} />
<Handle type="source" id="vial4" position="top" style={{ background: '#555', left: '80%' }} />


<div style={{
position: 'absolute',
right: '6px',
bottom: '50%',
fontSize: '12px',
fontWeight: 'normal',
textAlign: 'right'
}}>
out
</div>

<Handle type="source" id="sample_out" position="right" style={{ background: '#555' }} />
</div>
);
}
1 change: 0 additions & 1 deletion src/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions src/convert_to_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .custom_pathsim_blocks import (
Process,
Splitter,
Bubbler,
)
from .pathsim_utils import (
map_str_to_object,
Expand Down Expand Up @@ -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

Expand Down
135 changes: 135 additions & 0 deletions src/custom_pathsim_blocks.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from pathsim.blocks import Block, ODE
import pathsim.blocks
from pathsim import Subsystem, Interface, Connection
import numpy as np


Expand Down Expand Up @@ -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
39 changes: 38 additions & 1 deletion src/pathsim_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -46,6 +46,7 @@
"integrator": Integrator,
"function": Function,
"delay": Delay,
"bubbler": Bubbler,
}


Expand Down Expand Up @@ -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"]]
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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

Expand Down
Loading
Loading