From 016a8f0ed31fd69c4e166334ca9cf9bca73a2379 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Sat, 2 Aug 2025 22:03:39 -0400 Subject: [PATCH 01/14] added name_to_output attributes --- src/custom_pathsim_blocks.py | 56 +++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/src/custom_pathsim_blocks.py b/src/custom_pathsim_blocks.py index f9a51f3c..92fab957 100644 --- a/src/custom_pathsim_blocks.py +++ b/src/custom_pathsim_blocks.py @@ -5,6 +5,8 @@ class Process(ODE): + name_to_output_port = {"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 super().__init__( @@ -21,8 +23,12 @@ def update(self, t): mass_rate = 0 else: mass_rate = x / self.residence_time - # first output is the state, second is the rate of change (mass rate) - self.outputs.update_from_array([x, mass_rate]) + # 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 + # update the outputs + self.outputs.update_from_array(outputs) class Splitter(Block): @@ -41,6 +47,8 @@ def update(self, t): class Splitter2(Splitter): + name_to_output_port = {"source1": 0, "source2": 1} + def __init__(self, f1, f2): """ Splitter with two outputs, fractions are f1 and f2. @@ -49,6 +57,8 @@ def __init__(self, f1, f2): class Splitter3(Splitter): + name_to_output_port = {"source1": 0, "source2": 1, "source3": 2} + def __init__(self, f1, f2, f3): """ Splitter with three outputs, fractions are f1, f2 and f3. @@ -105,6 +115,18 @@ 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 = { + "vial1": 0, + "vial2": 1, + "vial3": 2, + "vial4": 3, + "sample_out": 4, + } + def __init__( self, conversion_efficiency=0.9, @@ -160,24 +182,28 @@ def __init__( interface, ] connections = [ - Connection(interface[0], col_eff1), + Connection( + interface[self.name_to_input_port["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[1], add1[1]), + Connection( + interface[self.name_to_input_port["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[0]), - Connection(vial_2, interface[1]), - Connection(vial_3, interface[2]), - Connection(vial_4, interface[3]), - Connection(add2, interface[4]), + 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"]]), ] super().__init__(blocks, connections) @@ -232,6 +258,9 @@ 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} + def __init__( self, thickness, temperature, D_0, E_D, surface_area=1, n_vertices=100 ): @@ -312,7 +341,9 @@ def update(self, t): # return 0.0 # block inputs - c_0, c_L = self.inputs.to_array() + 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"]] if t == 0.0: flux_0, flux_L = 0, 0 @@ -323,4 +354,7 @@ def update(self, t): flux_0 *= self.surface_area flux_L *= self.surface_area - return self.outputs.update_from_array([flux_0, flux_L]) + 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) From 0f214a88bd832f94d7edcb9167ff1435e2ba7225 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Sat, 2 Aug 2025 22:32:49 -0400 Subject: [PATCH 02/14] simplify pathsim_utils --- src/convert_to_python.py | 1 - src/pathsim_utils.py | 108 ++++----------------------------------- test/test_backend.py | 39 ++------------ 3 files changed, 14 insertions(+), 134 deletions(-) diff --git a/src/convert_to_python.py b/src/convert_to_python.py index 54057280..7d1eb7e4 100644 --- a/src/convert_to_python.py +++ b/src/convert_to_python.py @@ -12,7 +12,6 @@ from .pathsim_utils import ( map_str_to_object, make_blocks, - make_connections, make_global_variables, ) diff --git a/src/pathsim_utils.py b/src/pathsim_utils.py index bac6ceac..aecf5f55 100644 --- a/src/pathsim_utils.py +++ b/src/pathsim_utils.py @@ -79,35 +79,6 @@ def find_block_by_id(block_id: str, blocks) -> Block: return None -def create_integrator( - node: dict, eval_namespace: dict = None -) -> tuple[Block, list[Schedule]]: - if eval_namespace is None: - eval_namespace = globals() - - parameters = get_parameters_for_block_class( - Integrator, node, eval_namespace=eval_namespace - ) - - block = Integrator(**parameters) - # add events to reset integrator if needed - events = block.create_reset_events() - return block, events - - -def create_bubbler(node: dict) -> Bubbler: - """ - Create a Bubbler block based on the node data. - """ - # Extract parameters from node data - parameters = get_parameters_for_block_class(Bubbler, node, eval_namespace=globals()) - block = Bubbler(**parameters) - - events = block.create_reset_events() - - return block, events - - def make_labels_for_scope(node: dict, edges: list, nodes: list) -> list[str]: # 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"]] @@ -290,28 +261,13 @@ def make_blocks( for node in nodes: block_type = node["type"] - # Manual construction for some block types - if block_type == "integrator": - block, event_int = create_integrator(node, eval_namespace) - events.extend(event_int) - elif block_type == "scope": + # TODO scope should be handled in the same way as other blocks + if block_type == "scope": block = create_scope(node, edges, nodes) - elif block_type == "splitter2": - block = Splitter2( - f1=eval(node["data"]["f1"], eval_namespace), - f2=eval(node["data"]["f2"], eval_namespace), - ) - elif block_type == "splitter3": - block = Splitter3( - f1=eval(node["data"]["f1"], eval_namespace), - f2=eval(node["data"]["f2"], eval_namespace), - f3=eval(node["data"]["f3"], eval_namespace), - ) - elif block_type == "bubbler": - block, events_bubbler = create_bubbler(node) - events.extend(events_bubbler) - else: # try automated construction + else: block = auto_block_construction(node, eval_namespace) + if hasattr(block, "create_reset_events"): + events.extend(block.create_reset_events()) block.id = node["id"] block.label = node["data"]["label"] @@ -338,17 +294,7 @@ def make_connections(nodes, edges, blocks) -> list[Connection]: for edge in outgoing_edges: target_block = find_block_by_id(edge["target"], blocks=blocks) if isinstance(block, Process): - if edge["sourceHandle"] == "inv": - output_index = 0 - elif edge["sourceHandle"] == "mass_flow_rate": - output_index = 1 - assert block.residence_time != 0, ( - "Residence time must be non-zero for mass flow rate output." - ) - else: - raise ValueError( - f"Invalid source handle '{edge['sourceHandle']}' for {edge}." - ) + output_index = block.name_to_output_port[edge["sourceHandle"]] elif isinstance(block, Splitter): # Splitter outputs are always in order, so we can use the handle directly assert edge["sourceHandle"], edge @@ -358,29 +304,9 @@ def make_connections(nodes, edges, blocks) -> list[Connection]: 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}." - ) + output_index = block.name_to_output_port[edge["sourceHandle"]] 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}." - ) + output_index = block.name_to_output_port[edge["sourceHandle"]] elif isinstance(block, Function): # Function outputs are always in order, so we can use the handle directly assert edge["sourceHandle"], edge @@ -396,23 +322,9 @@ def make_connections(nodes, edges, blocks) -> list[Connection]: if isinstance(target_block, Scope): input_index = target_block._connections_order.index(edge["id"]) elif isinstance(target_block, Bubbler): - if edge["targetHandle"] == "sample_in_soluble": - input_index = 0 - elif edge["targetHandle"] == "sample_in_insoluble": - input_index = 1 - else: - raise ValueError( - f"Invalid target handle '{edge['targetHandle']}' for {edge}." - ) + input_index = target_block.name_to_input_port[edge["targetHandle"]] 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}." - ) + input_index = target_block.name_to_output_port[edge["targetHandle"]] elif isinstance(target_block, Function): # Function inputs are always in order, so we can use the handle directly input_index = int(edge["targetHandle"].replace("target-", "")) diff --git a/test/test_backend.py b/test/test_backend.py index 838837e1..59d5961b 100644 --- a/test/test_backend.py +++ b/test/test_backend.py @@ -1,7 +1,5 @@ from src.pathsim_utils import ( - create_integrator, auto_block_construction, - create_bubbler, create_scope, ) from src.custom_pathsim_blocks import Process, Splitter2, Splitter3, Bubbler, Integrator @@ -122,20 +120,6 @@ def _create_node(block_type: str, id: str = "1", data_overrides: dict = None): return _create_node -def test_create_integrator(): - node = { - "data": {"initial_value": "", "label": "IV vial 1", "reset_times": ""}, - "id": "9", - "type": "integrator", - } - integrator, events = create_integrator(node) - - assert isinstance(integrator, Integrator) - assert integrator.initial_value == 0 - for event in events: - assert isinstance(event, pathsim.blocks.Schedule) - - @pytest.mark.parametrize( "block_type,expected_class", [ @@ -173,6 +157,10 @@ def test_auto_block_construction(node_factory, block_type, expected_class): ("process", Process), ("white_noise", pathsim.blocks.noise.WhiteNoise), ("pink_noise", pathsim.blocks.noise.PinkNoise), + ("bubbler", Bubbler), + ("integrator", Integrator), + ("splitter2", Splitter2), + ("splitter3", Splitter3), ], ) def test_auto_block_construction_with_var(node_factory, block_type, expected_class): @@ -189,25 +177,6 @@ def test_auto_block_construction_with_var(node_factory, block_type, expected_cla assert isinstance(block, expected_class) -def test_create_bubbler(): - node = { - "id": "6", - "type": "bubbler", - "position": {"x": 627, "y": 357}, - "data": { - "label": "bubbler 6", - "conversion_efficiency": "", - "replacement_times": "[1, 2, 3]", - "vial_efficiency": "", - }, - "measured": {"width": 230, "height": 160}, - "selected": False, - "dragging": False, - } - block, events = create_bubbler(node) - assert isinstance(block, Bubbler) - - def test_make_scope(): node = { "id": "7", From 85c77540af071732fb02b012c4e51d8dab2dd789 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Sat, 2 Aug 2025 22:38:01 -0400 Subject: [PATCH 03/14] fixed test --- test/test_backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_backend.py b/test/test_backend.py index 59d5961b..fba795af 100644 --- a/test/test_backend.py +++ b/test/test_backend.py @@ -134,6 +134,8 @@ def _create_node(block_type: str, id: str = "1", data_overrides: dict = None): ("splitter3", Splitter3), ("white_noise", pathsim.blocks.noise.WhiteNoise), ("pink_noise", pathsim.blocks.noise.PinkNoise), + ("bubbler", Bubbler), + ("integrator", Integrator), ], ) def test_auto_block_construction(node_factory, block_type, expected_class): @@ -159,8 +161,6 @@ def test_auto_block_construction(node_factory, block_type, expected_class): ("pink_noise", pathsim.blocks.noise.PinkNoise), ("bubbler", Bubbler), ("integrator", Integrator), - ("splitter2", Splitter2), - ("splitter3", Splitter3), ], ) def test_auto_block_construction_with_var(node_factory, block_type, expected_class): From 61d166e4b479b745f798b6d594fa63c288537cc1 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Sat, 2 Aug 2025 22:44:12 -0400 Subject: [PATCH 04/14] removed create_scope --- src/pathsim_utils.py | 30 +++++++++--------------------- test/test_backend.py | 20 +++----------------- 2 files changed, 12 insertions(+), 38 deletions(-) diff --git a/src/pathsim_utils.py b/src/pathsim_utils.py index aecf5f55..23ff7bf0 100644 --- a/src/pathsim_utils.py +++ b/src/pathsim_utils.py @@ -109,18 +109,6 @@ def make_labels_for_scope(node: dict, edges: list, nodes: list) -> list[str]: return labels, connections_order -def create_scope(node: dict, edges, nodes) -> Scope: - block = auto_block_construction(node, eval_namespace=globals()) - - # override labels + add connections order - # TODO this should be done in "make connections" - labels, connections_order = make_labels_for_scope(node, edges, nodes) - block._connections_order = connections_order - block.labels = labels - - return block - - def make_global_variables(global_vars): # Validate and exec global variables so that they are usable later in this script. # Return a namespace dictionary containing the global variables @@ -259,15 +247,15 @@ def make_blocks( blocks, events = [], [] for node in nodes: - block_type = node["type"] - - # TODO scope should be handled in the same way as other blocks - if block_type == "scope": - block = create_scope(node, edges, nodes) - else: - block = auto_block_construction(node, eval_namespace) - if hasattr(block, "create_reset_events"): - events.extend(block.create_reset_events()) + block = auto_block_construction(node, eval_namespace) + if hasattr(block, "create_reset_events"): + events.extend(block.create_reset_events()) + + if isinstance(block, Scope): + if block.labels == []: + block.labels, block._connections_order = make_labels_for_scope( + node, edges, nodes + ) block.id = node["id"] block.label = node["data"]["label"] diff --git a/test/test_backend.py b/test/test_backend.py index fba795af..0ff13876 100644 --- a/test/test_backend.py +++ b/test/test_backend.py @@ -1,7 +1,4 @@ -from src.pathsim_utils import ( - auto_block_construction, - create_scope, -) +from src.pathsim_utils import auto_block_construction from src.custom_pathsim_blocks import Process, Splitter2, Splitter3, Bubbler, Integrator import pathsim.blocks @@ -64,7 +61,7 @@ }, "scope": { "type": "scope", - "data": {"label": "Scope", "sampling_rate": "", "labels": ""}, + "data": {"label": "scope 7", "sampling_rate": "", "labels": "", "t_wait": ""}, }, "white_noise": { "type": "white_noise", @@ -136,6 +133,7 @@ def _create_node(block_type: str, id: str = "1", data_overrides: dict = None): ("pink_noise", pathsim.blocks.noise.PinkNoise), ("bubbler", Bubbler), ("integrator", Integrator), + ("scope", pathsim.blocks.Scope), ], ) def test_auto_block_construction(node_factory, block_type, expected_class): @@ -175,15 +173,3 @@ def test_auto_block_construction_with_var(node_factory, block_type, expected_cla break block = auto_block_construction(node, eval_namespace={"var1": 5.5}) assert isinstance(block, expected_class) - - -def test_make_scope(): - node = { - "id": "7", - "type": "scope", - "data": {"label": "scope 7", "sampling_rate": "", "labels": "", "t_wait": ""}, - } - block = create_scope(node, edges=[], nodes=[node]) - assert isinstance(block, pathsim.blocks.Scope) - assert block.labels == [] - assert block.sampling_rate is None From 5673eb963eb16b55ff1b0f5aabd6efd10443111f Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Sat, 2 Aug 2025 22:51:35 -0400 Subject: [PATCH 05/14] simplified labels for scope --- src/pathsim_utils.py | 48 +++++++++----------------------------------- 1 file changed, 9 insertions(+), 39 deletions(-) diff --git a/src/pathsim_utils.py b/src/pathsim_utils.py index 23ff7bf0..43160946 100644 --- a/src/pathsim_utils.py +++ b/src/pathsim_utils.py @@ -79,36 +79,6 @@ def find_block_by_id(block_id: str, blocks) -> Block: return None -def make_labels_for_scope(node: dict, edges: list, nodes: list) -> list[str]: - # 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"]] - incoming_edges.sort(key=lambda x: x["source"]) - - # create labels for the scope based on incoming edges - labels = [] - duplicate_labels = [] - connections_order = [] # will be used later to make connections - for edge in incoming_edges: - source_node = find_node_by_id(edge["source"], nodes=nodes) - label = source_node["data"]["label"] - connections_order.append(edge["id"]) - - # If the label already exists, try to append the source handle to it (if it exists) - if label in labels or label in duplicate_labels: - duplicate_labels.append(label) - if edge["sourceHandle"]: - new_label = label + f" ({edge['sourceHandle']})" - label = new_label - labels.append(label) - - for i, (edge, label) in enumerate(zip(incoming_edges, labels)): - if label in duplicate_labels: - if edge["sourceHandle"]: - labels[i] += f" ({edge['sourceHandle']})" - - return labels, connections_order - - def make_global_variables(global_vars): # Validate and exec global variables so that they are usable later in this script. # Return a namespace dictionary containing the global variables @@ -251,12 +221,6 @@ def make_blocks( if hasattr(block, "create_reset_events"): events.extend(block.create_reset_events()) - if isinstance(block, Scope): - if block.labels == []: - block.labels, block._connections_order = make_labels_for_scope( - node, edges, nodes - ) - block.id = node["id"] block.label = node["data"]["label"] blocks.append(block) @@ -307,9 +271,7 @@ def make_connections(nodes, edges, blocks) -> list[Connection]: ) output_index = 0 - if isinstance(target_block, Scope): - input_index = target_block._connections_order.index(edge["id"]) - elif isinstance(target_block, Bubbler): + if isinstance(target_block, Bubbler): input_index = target_block.name_to_input_port[edge["targetHandle"]] elif isinstance(target_block, FestimWall): input_index = target_block.name_to_output_port[edge["targetHandle"]] @@ -324,6 +286,14 @@ def make_connections(nodes, edges, blocks) -> list[Connection]: ) input_index = block_to_input_index[target_block] + if isinstance(target_block, Scope): + if target_block.labels == []: + label = node["data"]["label"] + if edge["sourceHandle"]: + label += f" ({edge['sourceHandle']})" + target_block.labels.append(label) + input_index = block_to_input_index[target_block] + connection = Connection( block[output_index], target_block[input_index], From 6d60cbd185c3cfb72765a221a1f34a008de7e05d Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Sat, 2 Aug 2025 23:18:57 -0400 Subject: [PATCH 06/14] propagated changes to convert to python --- src/convert_to_python.py | 81 ++++++++--------------------------- src/pathsim_utils.py | 1 - src/templates/block_macros.py | 2 +- 3 files changed, 18 insertions(+), 66 deletions(-) diff --git a/src/convert_to_python.py b/src/convert_to_python.py index 7d1eb7e4..b5618ff6 100644 --- a/src/convert_to_python.py +++ b/src/convert_to_python.py @@ -74,18 +74,6 @@ def process_node_data(nodes: list[dict], edges: list[dict]) -> list[dict]: # Add expected arguments node["expected_arguments"] = signature(block_class).parameters - # if it's a scope, find labels - if node["type"] == "scope": - incoming_edges = [edge for edge in edges if edge["target"] == node["id"]] - incoming_edges.sort(key=lambda x: x["source"]) - node["labels"] = [] - for incoming_edge in incoming_edges: - source_node = next( - (n for n in nodes if n["id"] == incoming_edge["source"]) - ) - - # TODO take care of duplicated labels - node["labels"].append(source_node["data"]["label"]) return nodes @@ -128,17 +116,7 @@ def make_edge_data(data: dict) -> list[dict]: target_block = next((b for b in blocks if b.id == edge["target"])) target_node = next((n for n in data["nodes"] if n["id"] == edge["target"])) if isinstance(block, Process): - if edge["sourceHandle"] == "inv": - output_index = 0 - elif edge["sourceHandle"] == "mass_flow_rate": - output_index = 1 - assert block.residence_time != 0, ( - "Residence time must be non-zero for mass flow rate output." - ) - else: - raise ValueError( - f"Invalid source handle '{edge['sourceHandle']}' for {edge}." - ) + output_index = block.name_to_output_port[edge["sourceHandle"]] elif isinstance(block, Splitter): # Splitter outputs are always in order, so we can use the handle directly assert edge["sourceHandle"], edge @@ -148,29 +126,9 @@ def make_edge_data(data: dict) -> list[dict]: 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}." - ) + output_index = block.name_to_output_port[edge["sourceHandle"]] 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}." - ) + output_index = block.name_to_output_port[edge["sourceHandle"]] elif isinstance(block, Function): # Function outputs are always in order, so we can use the handle directly assert edge["sourceHandle"], edge @@ -183,26 +141,10 @@ def make_edge_data(data: dict) -> list[dict]: ) output_index = 0 - if isinstance(target_block, Scope): - input_index = target_block._connections_order.index(edge["id"]) - elif isinstance(target_block, Bubbler): - if edge["targetHandle"] == "sample_in_soluble": - input_index = 0 - elif edge["targetHandle"] == "sample_in_insoluble": - input_index = 1 - else: - raise ValueError( - f"Invalid target handle '{edge['targetHandle']}' for {edge}." - ) + if isinstance(target_block, Bubbler): + input_index = target_block.name_to_input_port[edge["targetHandle"]] 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}." - ) + input_index = target_block.name_to_output_port[edge["targetHandle"]] elif isinstance(target_block, Function): # Function inputs are always in order, so we can use the handle directly input_index = int(edge["targetHandle"].replace("target-", "")) @@ -214,6 +156,17 @@ def make_edge_data(data: dict) -> list[dict]: ) input_index = block_to_input_index[target_block] + # if it's a scope, find labels + if target_node["type"] == "scope": + if target_node["data"]["labels"] == "": + target_node["data"]["labels"] = [] + + if isinstance(target_node["data"]["labels"], list): + label = node["data"]["label"] + if edge["sourceHandle"]: + label += f" ({edge['sourceHandle']})" + target_node["data"]["labels"].append(label) + edge["source_var_name"] = node["var_name"] edge["target_var_name"] = target_node["var_name"] edge["source_port"] = f"[{output_index}]" diff --git a/src/pathsim_utils.py b/src/pathsim_utils.py index 43160946..bed8a5a6 100644 --- a/src/pathsim_utils.py +++ b/src/pathsim_utils.py @@ -292,7 +292,6 @@ def make_connections(nodes, edges, blocks) -> list[Connection]: if edge["sourceHandle"]: label += f" ({edge['sourceHandle']})" target_block.labels.append(label) - input_index = block_to_input_index[target_block] connection = Connection( block[output_index], diff --git a/src/templates/block_macros.py b/src/templates/block_macros.py index ce5c3164..ff4d2365 100644 --- a/src/templates/block_macros.py +++ b/src/templates/block_macros.py @@ -34,7 +34,7 @@ {% macro create_scope_block(node) -%} {{ node["var_name"] }} = pathsim.blocks.Scope( - labels={{ node["labels"] }} + labels={{ node["data"]["labels"] }} ) {%- endmacro -%} From 9da89728efb56d6de29a16224bcca02f4f9eeafc Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Sun, 3 Aug 2025 14:16:40 -0400 Subject: [PATCH 07/14] get_input_index and get_output_index functions --- src/convert_to_python.py | 45 ++-------------- src/pathsim_utils.py | 107 ++++++++++++++++++++++++--------------- 2 files changed, 70 insertions(+), 82 deletions(-) diff --git a/src/convert_to_python.py b/src/convert_to_python.py index b5618ff6..23dca752 100644 --- a/src/convert_to_python.py +++ b/src/convert_to_python.py @@ -13,6 +13,8 @@ map_str_to_object, make_blocks, make_global_variables, + get_input_index, + get_output_index, ) @@ -115,46 +117,9 @@ def make_edge_data(data: dict) -> list[dict]: for edge in outgoing_edges: target_block = next((b for b in blocks if b.id == edge["target"])) target_node = next((n for n in data["nodes"] if n["id"] == edge["target"])) - if isinstance(block, Process): - output_index = block.name_to_output_port[edge["sourceHandle"]] - elif isinstance(block, Splitter): - # Splitter outputs are always in order, so we can use the handle directly - assert edge["sourceHandle"], edge - output_index = int(edge["sourceHandle"].replace("source", "")) - 1 - if output_index >= block.n: - raise ValueError( - f"Invalid source handle '{edge['sourceHandle']}' for {edge}." - ) - elif isinstance(block, Bubbler): - output_index = block.name_to_output_port[edge["sourceHandle"]] - elif isinstance(block, FestimWall): - output_index = block.name_to_output_port[edge["sourceHandle"]] - elif isinstance(block, Function): - # Function outputs are always in order, so we can use the handle directly - assert edge["sourceHandle"], edge - output_index = int(edge["sourceHandle"].replace("source-", "")) - else: - # make sure that the source block has only one output port (ie. that sourceHandle is None) - assert edge["sourceHandle"] is None, ( - f"Source block {block.id} has multiple output ports, " - "but connection method hasn't been implemented." - ) - output_index = 0 - - if isinstance(target_block, Bubbler): - input_index = target_block.name_to_input_port[edge["targetHandle"]] - elif isinstance(target_block, FestimWall): - input_index = target_block.name_to_output_port[edge["targetHandle"]] - elif isinstance(target_block, Function): - # Function inputs are always in order, so we can use the handle directly - input_index = int(edge["targetHandle"].replace("target-", "")) - else: - # make sure that the target block has only one input port (ie. that targetHandle is None) - assert edge["targetHandle"] is None, ( - f"Target block {target_block.id} has multiple input ports, " - "but connection method hasn't been implemented." - ) - input_index = block_to_input_index[target_block] + + output_index = get_output_index(block, edge) + input_index = get_input_index(target_block, edge, block_to_input_index) # if it's a scope, find labels if target_node["type"] == "scope": diff --git a/src/pathsim_utils.py b/src/pathsim_utils.py index bed8a5a6..e6156009 100644 --- a/src/pathsim_utils.py +++ b/src/pathsim_utils.py @@ -228,6 +228,66 @@ def make_blocks( return blocks, events +def get_input_index(block: Block, edge: dict, block_to_input_index: dict) -> int: + """ + Get the input index for a block based on the edge data. + + Args: + block: The block object. + edge: The edge dictionary containing source and target information. + + Returns: + The input index for the block. + """ + if hasattr(block, "name_to_input_port"): + return block.name_to_input_port[edge["targetHandle"]] + elif isinstance(block, Function): + return int(edge["targetHandle"].replace("target-", "")) + else: + # make sure that the target block has only one input port (ie. that targetHandle is None) + assert edge["targetHandle"] is None, ( + f"Target block {block.id} has multiple input ports, " + "but connection method hasn't been implemented." + ) + return block_to_input_index[block] + + +# TODO here we could only pass edge and not block +def get_output_index(block: Block, edge: dict) -> int: + """ + Get the output index for a block based on the edge data. + + Args: + block: The block object. + edge: The edge dictionary containing source and target information. + + Returns: + The output index for the block. + """ + if hasattr(block, "name_to_output_port"): + return block.name_to_output_port[edge["sourceHandle"]] + elif isinstance(block, Splitter): + # Splitter outputs are always in order, so we can use the handle directly + assert edge["sourceHandle"], edge + output_index = int(edge["sourceHandle"].replace("source", "")) - 1 + if output_index >= block.n: + raise ValueError( + f"Invalid source handle '{edge['sourceHandle']}' for {edge}." + ) + return output_index + elif isinstance(block, Function): + # Function outputs are always in order, so we can use the handle directly + assert edge["sourceHandle"], edge + return int(edge["sourceHandle"].replace("source-", "")) + else: + # make sure that the source block has only one output port (ie. that sourceHandle is None) + assert edge["sourceHandle"] is None, ( + f"Source block {block.id} has multiple output ports, " + "but connection method hasn't been implemented." + ) + return 0 + + def make_connections(nodes, edges, blocks) -> list[Connection]: # Create connections based on the sorted edges to match beta order connections_pathsim = [] @@ -241,51 +301,14 @@ def make_connections(nodes, edges, blocks) -> list[Connection]: incoming_edges = [edge for edge in edges if edge["target"] == node["id"]] incoming_edges.sort(key=lambda x: x["source"]) - block = find_block_by_id(node["id"], blocks=blocks) + source_block = find_block_by_id(node["id"], blocks=blocks) for edge in outgoing_edges: target_block = find_block_by_id(edge["target"], blocks=blocks) - if isinstance(block, Process): - output_index = block.name_to_output_port[edge["sourceHandle"]] - elif isinstance(block, Splitter): - # Splitter outputs are always in order, so we can use the handle directly - assert edge["sourceHandle"], edge - output_index = int(edge["sourceHandle"].replace("source", "")) - 1 - if output_index >= block.n: - raise ValueError( - f"Invalid source handle '{edge['sourceHandle']}' for {edge}." - ) - elif isinstance(block, Bubbler): - output_index = block.name_to_output_port[edge["sourceHandle"]] - elif isinstance(block, FestimWall): - output_index = block.name_to_output_port[edge["sourceHandle"]] - elif isinstance(block, Function): - # Function outputs are always in order, so we can use the handle directly - assert edge["sourceHandle"], edge - output_index = int(edge["sourceHandle"].replace("source-", "")) - else: - # make sure that the source block has only one output port (ie. that sourceHandle is None) - assert edge["sourceHandle"] is None, ( - f"Source block {block.id} has multiple output ports, " - "but connection method hasn't been implemented." - ) - output_index = 0 - - if isinstance(target_block, Bubbler): - input_index = target_block.name_to_input_port[edge["targetHandle"]] - elif isinstance(target_block, FestimWall): - input_index = target_block.name_to_output_port[edge["targetHandle"]] - elif isinstance(target_block, Function): - # Function inputs are always in order, so we can use the handle directly - input_index = int(edge["targetHandle"].replace("target-", "")) - else: - # make sure that the target block has only one input port (ie. that targetHandle is None) - assert edge["targetHandle"] is None, ( - f"Target block {target_block.id} has multiple input ports, " - "but connection method hasn't been implemented." - ) - input_index = block_to_input_index[target_block] + output_index = get_output_index(source_block, edge) + input_index = get_input_index(target_block, edge, block_to_input_index) + # if it's a scope, add labels if not already present if isinstance(target_block, Scope): if target_block.labels == []: label = node["data"]["label"] @@ -294,7 +317,7 @@ def make_connections(nodes, edges, blocks) -> list[Connection]: target_block.labels.append(label) connection = Connection( - block[output_index], + source_block[output_index], target_block[input_index], ) connections_pathsim.append(connection) From c8bc03eb1a57884fb69e556105686f7ca7bbac4a Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Sun, 3 Aug 2025 14:26:28 -0400 Subject: [PATCH 08/14] removed edges arg --- src/pathsim_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pathsim_utils.py b/src/pathsim_utils.py index e6156009..4643e6c8 100644 --- a/src/pathsim_utils.py +++ b/src/pathsim_utils.py @@ -212,7 +212,7 @@ def get_parameters_for_block_class(block_class, node, eval_namespace): def make_blocks( - nodes: list[dict], edges: list[dict], eval_namespace: dict = None + nodes: list[dict], eval_namespace: dict = None ) -> tuple[list[Block], list[Event]]: blocks, events = [], [] @@ -365,7 +365,7 @@ def make_pathsim_model(graph_data: dict) -> tuple[Simulation, float]: ) # Create blocks - blocks, events = make_blocks(nodes, edges, eval_namespace) + blocks, events = make_blocks(nodes, eval_namespace) connections_pathsim = make_connections(nodes, edges, blocks) From c5e5dbf52b4623398655d109f66e11bdf499e3bb Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Sun, 3 Aug 2025 14:36:30 -0400 Subject: [PATCH 09/14] fix for scope labels --- src/pathsim_utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pathsim_utils.py b/src/pathsim_utils.py index 4643e6c8..82ea7bfa 100644 --- a/src/pathsim_utils.py +++ b/src/pathsim_utils.py @@ -294,6 +294,9 @@ def make_connections(nodes, edges, blocks) -> list[Connection]: # Process each node and its sorted incoming edges to create connections block_to_input_index = {b: 0 for b in blocks} + + scopes_without_labels = [] + for node in nodes: outgoing_edges = [edge for edge in edges if edge["source"] == node["id"]] outgoing_edges.sort(key=lambda x: x["target"]) @@ -311,6 +314,8 @@ def make_connections(nodes, edges, blocks) -> list[Connection]: # if it's a scope, add labels if not already present if isinstance(target_block, Scope): if target_block.labels == []: + scopes_without_labels.append(target_block) + if target_block in scopes_without_labels: label = node["data"]["label"] if edge["sourceHandle"]: label += f" ({edge['sourceHandle']})" From 45c706347f52d6bf71c7fd822017709967f9ca2f Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Sun, 3 Aug 2025 14:47:52 -0400 Subject: [PATCH 10/14] fixed make edge_data --- src/convert_to_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/convert_to_python.py b/src/convert_to_python.py index 23dca752..33b60e8d 100644 --- a/src/convert_to_python.py +++ b/src/convert_to_python.py @@ -102,7 +102,7 @@ def make_edge_data(data: dict) -> list[dict]: # we need the namespace since we call make_blocks namespace = make_global_variables(data["globalVariables"]) - blocks, _ = make_blocks(data["nodes"], data["edges"], eval_namespace=namespace) + blocks, _ = make_blocks(data["nodes"], eval_namespace=namespace) # Process each node and its sorted incoming edges to create connections block_to_input_index = {b: 0 for b in blocks} From ea39cfee9f9af59f150654696670fa2027e3a732 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Sun, 3 Aug 2025 19:50:37 -0400 Subject: [PATCH 11/14] if default values are empty dicts or list, make a copy --- src/pathsim_utils.py | 7 ++++++- test/test_backend.py | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/pathsim_utils.py b/src/pathsim_utils.py index 82ea7bfa..a088c856 100644 --- a/src/pathsim_utils.py +++ b/src/pathsim_utils.py @@ -205,7 +205,12 @@ def get_parameters_for_block_class(block_class, node, eval_namespace): raise ValueError( f"expected parameter for {k} in {node['type']} ({node['label']})" ) - parameters[k] = value.default + + # make a copy of the default value + if isinstance(value.default, (list, dict)): + parameters[k] = value.default.copy() + else: + parameters[k] = value.default else: parameters[k] = eval(user_input, eval_namespace) return parameters diff --git a/test/test_backend.py b/test/test_backend.py index 0ff13876..1360f425 100644 --- a/test/test_backend.py +++ b/test/test_backend.py @@ -1,4 +1,4 @@ -from src.pathsim_utils import auto_block_construction +from src.pathsim_utils import auto_block_construction, make_connections from src.custom_pathsim_blocks import Process, Splitter2, Splitter3, Bubbler, Integrator import pathsim.blocks @@ -173,3 +173,19 @@ def test_auto_block_construction_with_var(node_factory, block_type, expected_cla break block = auto_block_construction(node, eval_namespace={"var1": 5.5}) assert isinstance(block, expected_class) + + +def test_two_scopes_no_labels(node_factory): + """Test that two scopes with no labels are handled correctly.""" + scope1 = node_factory("scope", id="scope1") + scope2 = node_factory("scope", id="scope2") + + # Create blocks + block1 = auto_block_construction(scope1) + block2 = auto_block_construction(scope2) + + assert block1.labels == [] + assert block2.labels == [] + + block1.labels.append("Scope 1 Data") + assert block2.labels == [] From e6a3bbb67e742e2fec8ef54c4ebb132ed6d0c862 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Sun, 3 Aug 2025 19:50:51 -0400 Subject: [PATCH 12/14] new example with two FESTIM walls --- example_graphs/two_walls.json | 395 ++++++++++++++++++++++++++++++++++ 1 file changed, 395 insertions(+) create mode 100644 example_graphs/two_walls.json diff --git a/example_graphs/two_walls.json b/example_graphs/two_walls.json new file mode 100644 index 00000000..4b0c7f27 --- /dev/null +++ b/example_graphs/two_walls.json @@ -0,0 +1,395 @@ +{ + "nodes": [ + { + "id": "4", + "type": "wall", + "position": { + "x": 358, + "y": 240 + }, + "data": { + "label": "wall 4", + "D_0": "0.1", + "E_D": "0", + "n_vertices": "10", + "surface_area": "", + "temperature": "300", + "thickness": "2" + }, + "measured": { + "width": 70, + "height": 200 + }, + "selected": false, + "dragging": false + }, + { + "id": "19", + "type": "integrator", + "position": { + "x": 506, + "y": 417 + }, + "data": { + "label": "presure", + "initial_value": "0", + "reset_times": "" + }, + "measured": { + "width": 200, + "height": 48 + }, + "selected": false, + "dragging": false + }, + { + "id": "20", + "type": "wall", + "position": { + "x": 797, + "y": 244 + }, + "data": { + "label": "wall 20", + "D_0": "0.1", + "E_D": "0", + "n_vertices": "100", + "surface_area": "", + "temperature": "300", + "thickness": "2" + }, + "measured": { + "width": 70, + "height": 200 + }, + "selected": false, + "dragging": false + }, + { + "id": "21", + "type": "constant", + "position": { + "x": 867.040214603105, + "y": 119.34307354934961 + }, + "data": { + "label": "constant 21", + "value": "0" + }, + "measured": { + "width": 206, + "height": 54 + }, + "selected": false, + "dragging": false + }, + { + "id": "22", + "type": "scope", + "position": { + "x": 857.819281205265, + "y": 634.6385624105301 + }, + "data": { + "label": "Pressure scope", + "labels": "", + "sampling_rate": "", + "t_wait": "" + }, + "measured": { + "width": 120, + "height": 140 + }, + "selected": false, + "dragging": false + }, + { + "id": "23", + "type": "stepsource", + "position": { + "x": 214, + "y": 121 + }, + "data": { + "label": "stepsource 23", + "amplitude": "1", + "tau": "0.5" + }, + "measured": { + "width": 120, + "height": 120 + }, + "selected": false, + "dragging": false + }, + { + "id": "24", + "type": "scope", + "position": { + "x": 983, + "y": 351 + }, + "data": { + "label": "scope 24", + "labels": "", + "sampling_rate": "", + "t_wait": "" + }, + "measured": { + "width": 120, + "height": 140 + }, + "selected": false, + "dragging": false + }, + { + "id": "25", + "type": "scope", + "position": { + "x": 470.81928120526504, + "y": 512.6385624105301 + }, + "data": { + "label": "Wall 1 scope", + "labels": "", + "sampling_rate": "", + "t_wait": "" + }, + "measured": { + "width": 120, + "height": 140 + }, + "selected": false, + "dragging": false + } + ], + "edges": [ + { + "id": "e4-19-from_flux_L", + "source": "4", + "target": "19", + "sourceHandle": "flux_L", + "targetHandle": null, + "type": "smoothstep", + "data": {}, + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + }, + "selected": false + }, + { + "id": "e19-20-to_c_0", + "source": "19", + "target": "20", + "sourceHandle": null, + "targetHandle": "c_0", + "type": "smoothstep", + "data": {}, + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + }, + "selected": false + }, + { + "id": "e20-19-from_flux_0", + "source": "20", + "target": "19", + "sourceHandle": "flux_0", + "targetHandle": null, + "type": "smoothstep", + "data": {}, + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + }, + "selected": false + }, + { + "id": "e19-22", + "source": "19", + "target": "22", + "sourceHandle": null, + "targetHandle": null, + "type": "smoothstep", + "data": {}, + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + }, + "selected": false + }, + { + "id": "e21-20-to_c_L", + "source": "21", + "target": "20", + "sourceHandle": null, + "targetHandle": "c_L", + "type": "smoothstep", + "data": {}, + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } + }, + { + "id": "e19-4-to_c_L", + "source": "19", + "target": "4", + "sourceHandle": null, + "targetHandle": "c_L", + "type": "smoothstep", + "data": {}, + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } + }, + { + "id": "e23-4-to_c_0", + "source": "23", + "target": "4", + "sourceHandle": null, + "targetHandle": "c_0", + "type": "smoothstep", + "data": {}, + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } + }, + { + "id": "e23-22", + "source": "23", + "target": "22", + "sourceHandle": null, + "targetHandle": null, + "type": "smoothstep", + "data": {}, + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + }, + "selected": false + }, + { + "id": "e20-24-from_flux_L", + "source": "20", + "target": "24", + "sourceHandle": "flux_L", + "targetHandle": null, + "type": "smoothstep", + "data": {}, + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } + }, + { + "id": "e23-25", + "source": "23", + "target": "25", + "sourceHandle": null, + "targetHandle": null, + "type": "smoothstep", + "data": {}, + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } + }, + { + "id": "e4-25-from_flux_L", + "source": "4", + "target": "25", + "sourceHandle": "flux_L", + "targetHandle": null, + "type": "smoothstep", + "data": {}, + "style": { + "strokeWidth": 2, + "stroke": "#ECDFCC" + }, + "markerEnd": { + "type": "arrowclosed", + "width": 20, + "height": 20, + "color": "#ECDFCC" + } + } + ], + "nodeCounter": 26, + "solverParams": { + "dt": "0.1", + "dt_min": "1e-6", + "dt_max": "1.0", + "Solver": "SSPRK22", + "tolerance_fpi": "1e-6", + "iterations_max": "100", + "log": "true", + "simulation_duration": "3", + "extra_params": "{}" + }, + "globalVariables": [] +} \ No newline at end of file From d1cc518376db09e8d1ae8da30c335c1a0dfb64cd Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Sun, 3 Aug 2025 20:15:49 -0400 Subject: [PATCH 13/14] find functions --- src/convert_to_python.py | 15 +++++---------- src/pathsim_utils.py | 14 ++++---------- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/src/convert_to_python.py b/src/convert_to_python.py index 33b60e8d..5d4e1a33 100644 --- a/src/convert_to_python.py +++ b/src/convert_to_python.py @@ -2,19 +2,14 @@ import os from inspect import signature -from pathsim.blocks import Scope, Function -from .custom_pathsim_blocks import ( - Process, - Splitter, - Bubbler, - FestimWall, -) from .pathsim_utils import ( map_str_to_object, make_blocks, make_global_variables, get_input_index, get_output_index, + find_block_by_id, + find_node_by_id, ) @@ -112,11 +107,11 @@ def make_edge_data(data: dict) -> list[dict]: ] outgoing_edges.sort(key=lambda x: x["target"]) - block = next((b for b in blocks if b.id == node["id"])) + block = find_block_by_id(node["id"], blocks) for edge in outgoing_edges: - target_block = next((b for b in blocks if b.id == edge["target"])) - target_node = next((n for n in data["nodes"] if n["id"] == edge["target"])) + target_block = find_block_by_id(edge["target"], blocks) + target_node = find_node_by_id(edge["target"], data["nodes"]) output_index = get_output_index(block, edge) input_index = get_input_index(target_block, edge, block_to_input_index) diff --git a/src/pathsim_utils.py b/src/pathsim_utils.py index a088c856..0677bd7e 100644 --- a/src/pathsim_utils.py +++ b/src/pathsim_utils.py @@ -65,18 +65,12 @@ } -def find_node_by_id(node_id: str, nodes: list) -> dict: - for node in nodes: - if node["id"] == node_id: - return node - return None +def find_node_by_id(node_id: str, nodes: list[dict]) -> dict: + return next((node for node in nodes if node["id"] == node_id), None) -def find_block_by_id(block_id: str, blocks) -> Block: - for block in blocks: - if hasattr(block, "id") and block.id == block_id: - return block - return None +def find_block_by_id(block_id: str, blocks: list[Block]) -> Block: + return next((block for block in blocks if block.id == block_id), None) def make_global_variables(global_vars): From ba69b35975c23e6922b572c40bcaff0740806bab Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Sun, 3 Aug 2025 20:21:04 -0400 Subject: [PATCH 14/14] skip tests if festim not installed --- example_graphs/{two_walls.json => festim_two_walls.json} | 0 test/test_examples.py | 6 ++++++ 2 files changed, 6 insertions(+) rename example_graphs/{two_walls.json => festim_two_walls.json} (100%) diff --git a/example_graphs/two_walls.json b/example_graphs/festim_two_walls.json similarity index 100% rename from example_graphs/two_walls.json rename to example_graphs/festim_two_walls.json diff --git a/test/test_examples.py b/test/test_examples.py index af085d39..33044490 100644 --- a/test/test_examples.py +++ b/test/test_examples.py @@ -17,6 +17,12 @@ def test_example(filename): """ Test the example simulation defined in the given filename. """ + if "festim" in filename.stem.lower(): + try: + import festim as F + except ImportError: + pytest.skip("Festim examples are not yet supported in this test suite.") + with open(filename, "r") as f: graph_data = json.load(f)