Skip to content
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ classifiers = [
]
requires-python = ">=3.8"
dependencies = [
"pathsim>=0.7.0",
"pathsim>=0.8.0",
"matplotlib>=3.7.0",
"numpy>=1.24.0",
"plotly>=6.0",
Expand Down
12 changes: 9 additions & 3 deletions src/components/EventsTab.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
func_act: '',
tolerance: '1e-16'
},
'ScheduleList': {
times_evt: '',
func_act: '',
tolerance: '1e-16'
},
'ZeroCrossingDown': {
func_evt: '',
func_act: '',
Expand Down Expand Up @@ -47,7 +52,8 @@

const eventTypes = [
'Condition',
'Schedule',
'Schedule',
'ScheduleList',
'ZeroCrossing',
'ZeroCrossingUp',
'ZeroCrossingDown'
Expand Down Expand Up @@ -75,13 +81,13 @@
// Validate required fields based on event type

// For Schedule, func_act is required
if (currentEvent.type === 'Schedule' && !currentEvent.func_act) {
if (['Schedule', 'ScheduleList'].includes(currentEvent.type) && !currentEvent.func_act) {
alert('func_act is required for Schedule events');
return;
}

// For other event types, both func_evt and func_act are typically required
if (currentEvent.type !== 'Schedule' && (!currentEvent.func_evt || !currentEvent.func_act)) {
if (!['Schedule', 'ScheduleList'].includes(currentEvent.type) && (!currentEvent.func_evt || !currentEvent.func_act)) {
alert('Both func_evt and func_act are required for this event type');
return;
}
Expand Down Expand Up @@ -362,7 +368,7 @@
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{events.map((event, index) => (

Check failure on line 371 in src/components/EventsTab.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

'index' is defined but never used

Check failure on line 371 in src/components/EventsTab.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

'index' is defined but never used
<div
key={event.id}
style={{
Expand Down
48 changes: 45 additions & 3 deletions src/nodeConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import IntegratorNode from './components/nodes/IntegratorNode';
import AdderNode from './components/nodes/AdderNode';
import ScopeNode from './components/nodes/ScopeNode';
import StepSourceNode from './components/nodes/StepSourceNode';
import {createFunctionNode} from './components/nodes/FunctionNode';
import { createFunctionNode } from './components/nodes/FunctionNode';
import DefaultNode from './components/nodes/DefaultNode';
import MultiplierNode from './components/nodes/MultiplierNode';
import { Splitter2Node, Splitter3Node } from './components/nodes/Splitters';
Expand Down Expand Up @@ -60,6 +60,32 @@ export const nodeTypes = {
fir: DefaultNode
};

export const nodeMathTypes = {
sin: DefaultNode,
cos: DefaultNode,
sqrt: DefaultNode,
abs: DefaultNode,
pow: DefaultNode,
exp: DefaultNode,
log: DefaultNode,
log10: DefaultNode,
tan: DefaultNode,
sinh: DefaultNode,
cosh: DefaultNode,
tanh: DefaultNode,
atan: DefaultNode,
norm: DefaultNode,
mod: DefaultNode,
clip: DefaultNode,
}

// add nodeMathTypes to nodeTypes
Object.keys(nodeMathTypes).forEach(type => {
if (!nodeTypes[type]) {
nodeTypes[type] = nodeMathTypes[type];
}
});

// Node categories for better organization
export const nodeCategories = {
'Sources': {
Expand All @@ -71,7 +97,7 @@ export const nodeCategories = {
description: 'Signal processing and transformation nodes'
},
'Math': {
nodes: ['adder', 'multiplier', 'splitter2', 'splitter3'],
nodes: ['adder', 'multiplier', 'splitter2', 'splitter3'].concat(Object.keys(nodeMathTypes)),
description: 'Mathematical operation nodes'
},
'Control': {
Expand Down Expand Up @@ -124,8 +150,24 @@ export const getNodeDisplayName = (nodeType) => {
'scope': 'Scope',
'spectrum': 'Spectrum',
'differentiator': 'Differentiator',
'sin': 'Sine',
'cos': 'Cosine',
'sqrt': 'Square Root',
'abs': 'Absolute',
'pow': 'Power',
'exp': 'Exponential',
'log': 'Logarithm',
'log10': 'Logarithm (Base 10)',
'tan': 'Tangent',
'sinh': 'Hyperbolic Sine',
'cosh': 'Hyperbolic Cosine',
'tanh': 'Hyperbolic Tangent',
'atan': 'Inverse Tangent',
'norm': 'Normalization',
'mod': 'Modulo',
'clip': 'Clipping',
};

return displayNames[nodeType] || nodeType.charAt(0).toUpperCase() + nodeType.slice(1);
};

Expand Down
10 changes: 8 additions & 2 deletions src/python/convert_to_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,14 @@ def make_edge_data(data: dict) -> list[dict]:

edge["source_var_name"] = node["var_name"]
edge["target_var_name"] = target_node["var_name"]
edge["source_port"] = f"[{output_index}]"
edge["target_port"] = f"[{input_index}]"
if isinstance(output_index, str):
edge["source_port"] = f"['{output_index}']"
else:
edge["source_port"] = f"[{output_index}]"
if isinstance(input_index, str):
edge["target_port"] = f"['{input_index}']"
else:
edge["target_port"] = f"[{input_index}]"
block_to_input_index[target_block] += 1

return data["edges"]
Expand Down
101 changes: 52 additions & 49 deletions src/python/custom_pathsim_blocks.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from pathsim.blocks import Block, ODE
import pathsim.blocks
import pathsim.events
from pathsim import Subsystem, Interface, Connection
import numpy as np


class Process(ODE):
name_to_output_port = {"inv": 0, "mass_flow_rate": 1}
_port_map_out = {"inv": 0, "mass_flow_rate": 1}

def __init__(self, residence_time=0, initial_value=0, source_term=0):
alpha = -1 / residence_time if residence_time != 0 else 0
Expand All @@ -25,8 +26,8 @@ def update(self, t):
mass_rate = x / self.residence_time
# first output is the inv, second is the mass_flow_rate
outputs = [None, None]
outputs[self.name_to_output_port["inv"]] = x
outputs[self.name_to_output_port["mass_flow_rate"]] = mass_rate
outputs[self._port_map_out["inv"]] = x
outputs[self._port_map_out["mass_flow_rate"]] = mass_rate
# update the outputs
self.outputs.update_from_array(outputs)

Expand All @@ -47,7 +48,7 @@ def update(self, t):


class Splitter2(Splitter):
name_to_output_port = {"source1": 0, "source2": 1}
_port_map_out = {"source1": 0, "source2": 1}

def __init__(self, f1=0.5, f2=0.5):
"""
Expand All @@ -57,7 +58,7 @@ def __init__(self, f1=0.5, f2=0.5):


class Splitter3(Splitter):
name_to_output_port = {"source1": 0, "source2": 1, "source3": 2}
_port_map_out = {"source1": 0, "source2": 1, "source3": 2}

def __init__(self, f1=1 / 3, f2=1 / 3, f3=1 / 3):
"""
Expand All @@ -78,7 +79,7 @@ def __init__(self, initial_value=0.0, reset_times=None):
super().__init__(initial_value=initial_value)
self.reset_times = reset_times

def create_reset_events(self):
def create_reset_events(self) -> list[pathsim.events.ScheduleList]:
"""Create reset events for the integrator based on the reset times.

Raises:
Expand All @@ -98,10 +99,15 @@ def create_reset_events(self):
else:
raise ValueError("reset_times must be a single value or a list of times")

return [
pathsim.blocks.Schedule(t_start=t, t_end=t, func_act=self.reset)
for t in reset_times
]
def func_act(_):
self.reset()

# can be simplified after https://github.com/milanofthe/pathsim/pull/66
event = pathsim.events.ScheduleList(times_evt=reset_times, func_act=func_act)
event.func_act = func_act
event.t_start = 0
event.t_end = None
return [event]


# BUBBLER SYSTEM
Expand All @@ -115,17 +121,17 @@ class Bubbler(Subsystem):
n_soluble_vials: float
n_insoluble_vials: float

name_to_input_port = {
"sample_in_soluble": 0,
"sample_in_insoluble": 1,
}
name_to_output_port = {
_port_map_out = {
"vial1": 0,
"vial2": 1,
"vial3": 2,
"vial4": 3,
"sample_out": 4,
}
_port_map_in = {
"sample_in_soluble": 0,
"sample_in_insoluble": 1,
}

def __init__(
self,
Expand Down Expand Up @@ -163,7 +169,11 @@ def __init__(
add1 = pathsim.blocks.Adder()
add2 = pathsim.blocks.Adder()

# can be simplified when https://github.com/milanofthe/pathsim/pull/65 is merged
interface = Interface()
interface._port_map_in = self._port_map_out
interface._port_map_out = self._port_map_in
interface.__init__() # reinitialize to rebuild registers

self.vials = [vial_1, vial_2, vial_3, vial_4]

Expand All @@ -182,46 +192,41 @@ def __init__(
interface,
]
connections = [
Connection(
interface[self.name_to_input_port["sample_in_soluble"]], col_eff1
),
Connection(interface["sample_in_soluble"], 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[self.name_to_input_port["sample_in_insoluble"]], add1[1]
),
Connection(interface["sample_in_insoluble"], 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[self.name_to_output_port["vial1"]]),
Connection(vial_2, interface[self.name_to_output_port["vial2"]]),
Connection(vial_3, interface[self.name_to_output_port["vial3"]]),
Connection(vial_4, interface[self.name_to_output_port["vial4"]]),
Connection(add2, interface[self.name_to_output_port["sample_out"]]),
Connection(vial_1, interface["vial1"]),
Connection(vial_2, interface["vial2"]),
Connection(vial_3, interface["vial3"]),
Connection(vial_4, interface["vial4"]),
Connection(add2, interface["sample_out"]),
]
super().__init__(blocks, connections)

def _create_reset_events_one_vial(
self, block, reset_times
) -> list[pathsim.blocks.Schedule]:
events = []

) -> pathsim.events.ScheduleList:
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
event = pathsim.events.ScheduleList(times_evt=reset_times, func_act=reset_itg)
# won't be needed after https://github.com/milanofthe/pathsim/pull/66
event.func_act = reset_itg
event.t_start = 0
event.t_end = None
return event

def create_reset_events(self) -> list[pathsim.blocks.Schedule]:
def create_reset_events(self) -> list[pathsim.events.ScheduleList]:
"""Create reset events for all vials based on the replacement times.

Raises:
Expand Down Expand Up @@ -249,7 +254,7 @@ def create_reset_events(self) -> list[pathsim.blocks.Schedule]:
"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]))
events.append(self._create_reset_events_one_vial(vial, reset_times[i]))

return events

Expand All @@ -259,20 +264,20 @@ def create_reset_events(self) -> list[pathsim.blocks.Schedule]:


class FestimWall(Block):
name_to_output_port = {"flux_0": 0, "flux_L": 1}
name_to_input_port = {"c_0": 0, "c_L": 1}
_port_map_out = {"flux_0": 0, "flux_L": 1}
_port_map_in = {"c_0": 0, "c_L": 1}

def __init__(
self, thickness, temperature, D_0, E_D, surface_area=1, n_vertices=100
):
super().__init__()
try:
import festim as F
except ImportError:
raise ImportError("festim is needed for FestimWall node.")
super().__init__()

self.inputs = Register(size=2)
self.outputs = Register(size=2)
self.inputs = Register(size=2, mapping=self._port_map_in)
self.outputs = Register(size=2, mapping=self._port_map_out)

self.thickness = thickness
self.temperature = temperature
Expand Down Expand Up @@ -345,10 +350,8 @@ def update(self, t):
# return 0.0

# block inputs
inputs = self.inputs.to_array()
c_0 = inputs[self.name_to_input_port["c_0"]]
c_L = inputs[self.name_to_input_port["c_L"]]
# print(c_0, c_L)
c_0 = self.inputs["c_0"]
c_L = self.inputs["c_L"]

if t == 0.0:
flux_0, flux_L = 0, 0
Expand All @@ -359,7 +362,7 @@ def update(self, t):

flux_0 *= self.surface_area
flux_L *= self.surface_area
outputs = [None, None]
outputs[self.name_to_output_port["flux_0"]] = flux_0
outputs[self.name_to_output_port["flux_L"]] = flux_L
return self.outputs.update_from_array(outputs)

self.outputs["flux_0"] = flux_0
self.outputs["flux_L"] = flux_L
return self.outputs
Loading
Loading