From 749402b20792c69581cac377c161f2b81f0aacef Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Wed, 3 Dec 2025 10:05:12 +0200 Subject: [PATCH 01/73] WIP, need to fix the bug that runs all the graphs when i run only one --- .../__init__.py | 0 .../epmty_node.py | 14 ++++++ .../graph_with_loop.py | 28 +++++++++++ .../graph_with_old_building_method.py | 26 ++++++++++ .../graph_with_one_nested_graph.py | 34 +++++++++++++ .../graph_with_two_nested_graphs.py | 49 +++++++++++++++++++ .../long_graph.py | 31 ++++++++++++ .../test_qualibration_graph/test_graph.py | 19 +++++++ tests/utils/local_qualibration_library.py | 31 ++++++++++++ 9 files changed, 232 insertions(+) create mode 100644 tests/calibration_nodes_and_graphs_for_testing/__init__.py create mode 100644 tests/calibration_nodes_and_graphs_for_testing/epmty_node.py create mode 100644 tests/calibration_nodes_and_graphs_for_testing/graph_with_loop.py create mode 100644 tests/calibration_nodes_and_graphs_for_testing/graph_with_old_building_method.py create mode 100644 tests/calibration_nodes_and_graphs_for_testing/graph_with_one_nested_graph.py create mode 100644 tests/calibration_nodes_and_graphs_for_testing/graph_with_two_nested_graphs.py create mode 100644 tests/calibration_nodes_and_graphs_for_testing/long_graph.py create mode 100644 tests/utils/local_qualibration_library.py diff --git a/tests/calibration_nodes_and_graphs_for_testing/__init__.py b/tests/calibration_nodes_and_graphs_for_testing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/calibration_nodes_and_graphs_for_testing/epmty_node.py b/tests/calibration_nodes_and_graphs_for_testing/epmty_node.py new file mode 100644 index 0000000..a23247e --- /dev/null +++ b/tests/calibration_nodes_and_graphs_for_testing/epmty_node.py @@ -0,0 +1,14 @@ +from pydantic import Field + +from qualibrate import NodeParameters, QualibrationNode + + +class Parameters(NodeParameters): + qubits: list[str] = Field(default_factory=list) + str_value: str = "test" + int_value: int = 1 + float_value: float = 1.0 + + +node = QualibrationNode("test_node", parameters=Parameters()) +node.parameters = Parameters() diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop.py new file mode 100644 index 0000000..365f87a --- /dev/null +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop.py @@ -0,0 +1,28 @@ +from typing import ClassVar + +from qualibrate import ( + logger, +) +from qualibrate.orchestration.basic_orchestrator import BasicOrchestrator +from qualibrate import GraphParameters, QualibrationGraph, QualibrationLibrary + +library = QualibrationLibrary.get_active_library() + +# Use the context manager to build the graph +with QualibrationGraph.build( + "graph_with_loop", + # description="An adaptive Ramsey experiment that repeats until T2* meets the target.", +) as graph: + # Get the Ramsey node from the library + graph.add_node(library.nodes.get_nocopy("test_node").copy(name="node")) + graph.add_node(library.nodes.get_nocopy("test_node").copy(name="node2")) + graph.loop( + "node", + max_iterations=10, + ) + graph.connect("node", "node2") + +# graph.run() + + # Set up the adaptive loop on the Ramsey node + diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_old_building_method.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_old_building_method.py new file mode 100644 index 0000000..0626025 --- /dev/null +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_old_building_method.py @@ -0,0 +1,26 @@ +from qualibrate.orchestration.basic_orchestrator import BasicOrchestrator +from qualibrate.parameters import GraphParameters +from qualibrate.qualibration_graph import QualibrationGraph +from qualibrate.qualibration_library import QualibrationLibrary +USED_NODE = "test_node" +library = QualibrationLibrary.get_active_library() + + +class Parameters(GraphParameters): + qubits: list[str] + + +nodes = { + "node1": library.nodes.get_nocopy(USED_NODE).copy(name="node1"), + "node2": library.nodes.get_nocopy(USED_NODE).copy(name="node2"), + "node3": library.nodes.get_nocopy(USED_NODE).copy(name="node3"), +} + +g = QualibrationGraph( + name="graph_with_old_building_method", + parameters=Parameters(qubits=[]), + nodes=nodes, + connectivity=[("node1", "node2")], + orchestrator=BasicOrchestrator(skip_failed=True), +) +# g.run() diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_one_nested_graph.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_one_nested_graph.py new file mode 100644 index 0000000..25c6034 --- /dev/null +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_one_nested_graph.py @@ -0,0 +1,34 @@ +from typing import ClassVar + +from qualibrate import ( + logger, +) +from qualibrate.orchestration.basic_orchestrator import BasicOrchestrator +from qualibrate import GraphParameters, QualibrationGraph, QualibrationLibrary + +library = QualibrationLibrary.get_active_library() + +class Parameters(GraphParameters): + targets_name: ClassVar[str] = "qubits_1" + qubits_1: list[str] + +with QualibrationGraph.build( + name="graph_with_one_nested_graph", + parameters=Parameters(qubits_1=["q1"]), + orchestrator=BasicOrchestrator(skip_failed=True), + ) as graph: + #create first node + graph.add_node(library.nodes.get_nocopy("test_node").copy(name=f"node_1")) + #create first subgraph + with QualibrationGraph.build( + name="subg", + parameters=Parameters(qubits_1="q2"), + orchestrator=BasicOrchestrator(skip_failed=True), + ) as subg: + subg.add_node(library.nodes.get_nocopy("test_node").copy(name=f"node_2")) + subg.add_node(library.nodes.get_nocopy("test_node").copy(name=f"node_3")) + subg.connect(src="node_2", dst="node_3") + graph.add_node(subg) + graph.add_node(library.nodes.get_nocopy("test_node").copy(name=f"node_4")) + graph.connect(src="node_1", dst=subg) + graph.connect(src=subg, dst="node_4") \ No newline at end of file diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_two_nested_graphs.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_two_nested_graphs.py new file mode 100644 index 0000000..696dde6 --- /dev/null +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_two_nested_graphs.py @@ -0,0 +1,49 @@ +from typing import ClassVar + +from qualibrate import ( + logger, +) +from qualibrate.orchestration.basic_orchestrator import BasicOrchestrator +from qualibrate import GraphParameters, QualibrationGraph, QualibrationLibrary + +library = QualibrationLibrary.get_active_library() + +class Parameters(GraphParameters): + targets_name: ClassVar[str] = "qubits_1" + qubits_1: list[str] + +with QualibrationGraph.build( + name="graph_with_two_nested_graphs", + parameters=Parameters(qubits_1=["q1"]), + orchestrator=BasicOrchestrator(skip_failed=True), + ) as graph: + #create first node + graph.add_node(library.nodes.get_nocopy("test_node").copy(name=f"node_1")) + #create first subgraph + with QualibrationGraph.build( + name="subg", + parameters=Parameters(qubits_1="q2"), + orchestrator=BasicOrchestrator(skip_failed=True), + ) as subg: + subg.add_node(library.nodes.get_nocopy("test_node").copy(name=f"node_2")) + subg.add_node(library.nodes.get_nocopy("test_node").copy(name=f"node_3")) + subg.connect(src="node_2", dst="node_3") + graph.add_node(subg) + #second subgraph + with QualibrationGraph.build( + name="subg2", + parameters=Parameters(qubits_1="q3"), + orchestrator=BasicOrchestrator(skip_failed=True), + ) as subg2: + subg2.add_node(library.nodes.get_nocopy("test_node").copy(name=f"node_4")) + subg2.add_node(library.nodes.get_nocopy("test_node").copy(name=f"node_5")) + subg2.connect(src="node_4", dst="node_5") + #doing connections + graph.add_node(subg2) + graph.connect(src="node_1", dst=subg) + graph.connect(src=subg, dst=subg2) +# graph.run() + + + + diff --git a/tests/calibration_nodes_and_graphs_for_testing/long_graph.py b/tests/calibration_nodes_and_graphs_for_testing/long_graph.py new file mode 100644 index 0000000..8f0a2d1 --- /dev/null +++ b/tests/calibration_nodes_and_graphs_for_testing/long_graph.py @@ -0,0 +1,31 @@ +from typing import ClassVar + +from qualibrate import ( + logger, +) +from qualibrate.orchestration.basic_orchestrator import BasicOrchestrator +from qualibrate import GraphParameters, QualibrationGraph, QualibrationLibrary + +library = QualibrationLibrary.get_active_library() + + +class Parameters(GraphParameters): + targets_name: ClassVar[str] = "qubits_1" + qubits_1: list[str] + + +logger.info("-------- start file----") +with QualibrationGraph.build( + "long_graph", + parameters=Parameters(qubits_1=[f"q{i}" for i in range(3)]), + orchestrator=BasicOrchestrator(skip_failed=True), +) as subg: + subg.add_node(library.nodes.get_nocopy("test_node").copy(name=f"node_1")) + for i in range(2,150): + subg.add_node(library.nodes.get_nocopy("test_node").copy(name=f"node_{i}")) + subg.connect(src=f"node_{i-1}", dst=f"node_{i}") + +subg.run() +# # node1 = library.nodes["test_node"] +# if __name__ == "__main__": +# print(45) \ No newline at end of file diff --git a/tests/unit/test_qualibration_graph/test_graph.py b/tests/unit/test_qualibration_graph/test_graph.py index 0a09b58..9cfa278 100644 --- a/tests/unit/test_qualibration_graph/test_graph.py +++ b/tests/unit/test_qualibration_graph/test_graph.py @@ -665,3 +665,22 @@ def test__validate_graph_acyclic_without_cycle( orchestrator=mock_orchestrator, ) graph._validate_graph_acyclic() + + def test_connect_then_connect_on_failure_same_edge_overwrites( + self, pre_setup_graph_init, mock_library + ): + """Test that calling connect() then connect_on_failure() + on the same edge overwrites to FAILED scenario""" + (nodes, _, _, _) = pre_setup_graph_init + + with QualibrationGraph.build( + name="test_graph", + parameters=GraphParameters(), + ) as graph: + graph.add_nodes(nodes["node1"], nodes["node2"]) + graph.connect_on_failure("node1", "node2") # Then: FAILED + graph.connect("node1", "node2") # First: SUCCESS + + # Should only have one edge with FAILED outcome (last write wins) + assert len(graph._connectivity) == 1 + assert graph._connectivity[("node1", "node2")] == Outcome.FAILED diff --git a/tests/utils/local_qualibration_library.py b/tests/utils/local_qualibration_library.py new file mode 100644 index 0000000..878ff1b --- /dev/null +++ b/tests/utils/local_qualibration_library.py @@ -0,0 +1,31 @@ +from unittest.mock import patch +from pathlib import Path +from qualibrate.qualibration_library import QualibrationLibrary + +def load_local_qualibration_library(config): + """ + Creates a QualibrationLibrary with get_qualibrate_config and + get_qualibrate_config_path patched to your desired values. + """ + from qualibrate import qualibration_node + + ctx1 = patch("qualibrate.qualibration_node.get_qualibrate_config_path") + ctx2 = patch( + "qualibrate.qualibration_node.get_qualibrate_config", + return_value=config, + ) + + # Return a context manager that patches AND yields the library + class PatchedLibraryContext: + def __enter__(self): + self._first_enter_result = ctx1.__enter__() + self._second_enter_result = ctx2.__enter__() + cal_path = Path(__file__).parent / "calibration_nodes_and_graphs_for_testing" + self.lib = QualibrationLibrary(cal_path) + return self.lib + + def __exit__(self, exc_type, exc, tb): + ctx1.__exit__(exc_type, exc, tb) + ctx2.__exit__(exc_type, exc, tb) + + return PatchedLibraryContext() \ No newline at end of file From d617774076c8393120f5549b2f3a66fd3f631c6a Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Wed, 3 Dec 2025 20:43:37 +0200 Subject: [PATCH 02/73] need to remove unndecessary code before merging --- .../empty_node2.py | 14 ++++++ .../epmty_node.py | 5 ++ .../graph_with_loop.py | 21 ++------ .../graph_with_loop_condition_function.py | 20 ++++++++ .../graph_with_loop_condition_generatior.py | 22 +++++++++ .../graph_with_loop_condition_lambda.py | 15 ++++++ .../graph_with_old_building_method.py | 8 +--- .../graph_with_one_nested_graph.py | 28 +++++------ .../graph_with_two_nested_graphs.py | 48 +++++++++---------- .../long_graph.py | 25 ++++------ .../test_qualibration_graph/test_graph.py | 9 ++-- tests/utils/local_qualibration_library.py | 12 +++-- 12 files changed, 141 insertions(+), 86 deletions(-) create mode 100644 tests/calibration_nodes_and_graphs_for_testing/empty_node2.py create mode 100644 tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_function.py create mode 100644 tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_generatior.py create mode 100644 tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py diff --git a/tests/calibration_nodes_and_graphs_for_testing/empty_node2.py b/tests/calibration_nodes_and_graphs_for_testing/empty_node2.py new file mode 100644 index 0000000..03a52ac --- /dev/null +++ b/tests/calibration_nodes_and_graphs_for_testing/empty_node2.py @@ -0,0 +1,14 @@ +from pydantic import Field + +from qualibrate import NodeParameters, QualibrationNode + + +class Parameters(NodeParameters): + qubits: list[str] = Field(default_factory=list) + str_value: str = "test" + int_value: int = 1 + float_value: float = 1.0 + + +node = QualibrationNode("test_node2", parameters=Parameters()) +node.parameters = Parameters() diff --git a/tests/calibration_nodes_and_graphs_for_testing/epmty_node.py b/tests/calibration_nodes_and_graphs_for_testing/epmty_node.py index a23247e..88b780a 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/epmty_node.py +++ b/tests/calibration_nodes_and_graphs_for_testing/epmty_node.py @@ -12,3 +12,8 @@ class Parameters(NodeParameters): node = QualibrationNode("test_node", parameters=Parameters()) node.parameters = Parameters() + + +@node.run_action() +def something(node: QualibrationNode): + node.log("node is running") diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop.py index 365f87a..54ae7dd 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop.py +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop.py @@ -1,28 +1,15 @@ -from typing import ClassVar - -from qualibrate import ( - logger, -) -from qualibrate.orchestration.basic_orchestrator import BasicOrchestrator -from qualibrate import GraphParameters, QualibrationGraph, QualibrationLibrary +from qualibrate import QualibrationGraph, QualibrationLibrary library = QualibrationLibrary.get_active_library() +USED_NODE = "test_node" -# Use the context manager to build the graph with QualibrationGraph.build( "graph_with_loop", - # description="An adaptive Ramsey experiment that repeats until T2* meets the target.", ) as graph: - # Get the Ramsey node from the library - graph.add_node(library.nodes.get_nocopy("test_node").copy(name="node")) - graph.add_node(library.nodes.get_nocopy("test_node").copy(name="node2")) + graph.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node")) + graph.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node2")) graph.loop( "node", max_iterations=10, ) graph.connect("node", "node2") - -# graph.run() - - # Set up the adaptive loop on the Ramsey node - diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_function.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_function.py new file mode 100644 index 0000000..a74cdb9 --- /dev/null +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_function.py @@ -0,0 +1,20 @@ +from qualibrate import QualibrationGraph, QualibrationLibrary, QualibrationNode + +library = QualibrationLibrary.get_active_library() +USED_NODE = "test_node" + + +def check_fidelity(node: QualibrationNode, target: str): + return node.results[target]["fidelity"] < 0.95 + + +with QualibrationGraph.build( + "graph_with_loop", +) as graph: + graph.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node")) + graph.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node2")) + graph.loop( + "node", + on=check_fidelity, + ) + graph.connect("node", "node2") diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_generatior.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_generatior.py new file mode 100644 index 0000000..6ba66db --- /dev/null +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_generatior.py @@ -0,0 +1,22 @@ +from qualibrate import QualibrationGraph, QualibrationLibrary, QualibrationNode + +library = QualibrationLibrary.get_active_library() +USED_NODE = "test_node" + + +def check_fidelity(node: QualibrationNode, target: str): + while True: + yield node.results[target]["fidelity"] < 0.95 + + +with QualibrationGraph.build( + "graph_with_loop", +) as graph: + graph.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node")) + graph.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node2")) + graph.loop( + "node", + on=check_fidelity, + max_iterations=10, + ) + graph.connect("node", "node2") diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py new file mode 100644 index 0000000..0f23390 --- /dev/null +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py @@ -0,0 +1,15 @@ +from qualibrate import QualibrationGraph, QualibrationLibrary + +library = QualibrationLibrary.get_active_library() +USED_NODE = "test_node" + +with QualibrationGraph.build( + "graph_with_loop", +) as graph: + graph.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node")) + graph.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node2")) + graph.loop( + "node", + on=lambda node, target: node.results[target]["fidelity"] < 0.95, + ) + graph.connect("node", "node2") diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_old_building_method.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_old_building_method.py index 0626025..b84f568 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/graph_with_old_building_method.py +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_old_building_method.py @@ -1,7 +1,5 @@ -from qualibrate.orchestration.basic_orchestrator import BasicOrchestrator -from qualibrate.parameters import GraphParameters -from qualibrate.qualibration_graph import QualibrationGraph -from qualibrate.qualibration_library import QualibrationLibrary +from qualibrate import GraphParameters, QualibrationGraph, QualibrationLibrary + USED_NODE = "test_node" library = QualibrationLibrary.get_active_library() @@ -21,6 +19,4 @@ class Parameters(GraphParameters): parameters=Parameters(qubits=[]), nodes=nodes, connectivity=[("node1", "node2")], - orchestrator=BasicOrchestrator(skip_failed=True), ) -# g.run() diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_one_nested_graph.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_one_nested_graph.py index 25c6034..8a792df 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/graph_with_one_nested_graph.py +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_one_nested_graph.py @@ -1,34 +1,30 @@ from typing import ClassVar -from qualibrate import ( - logger, -) -from qualibrate.orchestration.basic_orchestrator import BasicOrchestrator from qualibrate import GraphParameters, QualibrationGraph, QualibrationLibrary library = QualibrationLibrary.get_active_library() + class Parameters(GraphParameters): targets_name: ClassVar[str] = "qubits_1" qubits_1: list[str] + with QualibrationGraph.build( - name="graph_with_one_nested_graph", - parameters=Parameters(qubits_1=["q1"]), - orchestrator=BasicOrchestrator(skip_failed=True), - ) as graph: - #create first node - graph.add_node(library.nodes.get_nocopy("test_node").copy(name=f"node_1")) - #create first subgraph + name="graph_with_one_nested_graph", + parameters=Parameters(qubits_1=["q1"]), +) as graph: + # create first node + graph.add_node(library.nodes.get_nocopy("test_node").copy(name="node_1")) + # create first subgraph with QualibrationGraph.build( name="subg", parameters=Parameters(qubits_1="q2"), - orchestrator=BasicOrchestrator(skip_failed=True), ) as subg: - subg.add_node(library.nodes.get_nocopy("test_node").copy(name=f"node_2")) - subg.add_node(library.nodes.get_nocopy("test_node").copy(name=f"node_3")) + subg.add_node(library.nodes.get_nocopy("test_node").copy(name="node_2")) + subg.add_node(library.nodes.get_nocopy("test_node").copy(name="node_3")) subg.connect(src="node_2", dst="node_3") graph.add_node(subg) - graph.add_node(library.nodes.get_nocopy("test_node").copy(name=f"node_4")) + graph.add_node(library.nodes.get_nocopy("test_node").copy(name="node_4")) graph.connect(src="node_1", dst=subg) - graph.connect(src=subg, dst="node_4") \ No newline at end of file + graph.connect(src=subg, dst="node_4") diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_two_nested_graphs.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_two_nested_graphs.py index 696dde6..1ab9c1d 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/graph_with_two_nested_graphs.py +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_two_nested_graphs.py @@ -1,49 +1,49 @@ from typing import ClassVar -from qualibrate import ( - logger, -) -from qualibrate.orchestration.basic_orchestrator import BasicOrchestrator from qualibrate import GraphParameters, QualibrationGraph, QualibrationLibrary library = QualibrationLibrary.get_active_library() + class Parameters(GraphParameters): targets_name: ClassVar[str] = "qubits_1" qubits_1: list[str] + with QualibrationGraph.build( - name="graph_with_two_nested_graphs", - parameters=Parameters(qubits_1=["q1"]), - orchestrator=BasicOrchestrator(skip_failed=True), - ) as graph: - #create first node - graph.add_node(library.nodes.get_nocopy("test_node").copy(name=f"node_1")) - #create first subgraph + name="graph_with_two_nested_graphs", + parameters=Parameters(qubits_1=["q1"]), +) as graph: + # create first node + + graph.add_node(library.nodes.get_nocopy("test_node").copy(name="node_1")) + + # create first subgraph + with QualibrationGraph.build( name="subg", parameters=Parameters(qubits_1="q2"), - orchestrator=BasicOrchestrator(skip_failed=True), ) as subg: - subg.add_node(library.nodes.get_nocopy("test_node").copy(name=f"node_2")) - subg.add_node(library.nodes.get_nocopy("test_node").copy(name=f"node_3")) + subg.add_node(library.nodes.get_nocopy("test_node").copy(name="node_2")) + subg.add_node(library.nodes.get_nocopy("test_node").copy(name="node_3")) subg.connect(src="node_2", dst="node_3") graph.add_node(subg) - #second subgraph + + # second subgraph + with QualibrationGraph.build( name="subg2", parameters=Parameters(qubits_1="q3"), - orchestrator=BasicOrchestrator(skip_failed=True), ) as subg2: - subg2.add_node(library.nodes.get_nocopy("test_node").copy(name=f"node_4")) - subg2.add_node(library.nodes.get_nocopy("test_node").copy(name=f"node_5")) + subg2.add_node( + library.nodes.get_nocopy("test_node").copy(name="node_4") + ) + subg2.add_node( + library.nodes.get_nocopy("test_node").copy(name="node_5") + ) subg2.connect(src="node_4", dst="node_5") - #doing connections + + # doing connections graph.add_node(subg2) graph.connect(src="node_1", dst=subg) graph.connect(src=subg, dst=subg2) -# graph.run() - - - - diff --git a/tests/calibration_nodes_and_graphs_for_testing/long_graph.py b/tests/calibration_nodes_and_graphs_for_testing/long_graph.py index 8f0a2d1..41bff3a 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/long_graph.py +++ b/tests/calibration_nodes_and_graphs_for_testing/long_graph.py @@ -1,9 +1,5 @@ from typing import ClassVar -from qualibrate import ( - logger, -) -from qualibrate.orchestration.basic_orchestrator import BasicOrchestrator from qualibrate import GraphParameters, QualibrationGraph, QualibrationLibrary library = QualibrationLibrary.get_active_library() @@ -14,18 +10,17 @@ class Parameters(GraphParameters): qubits_1: list[str] -logger.info("-------- start file----") with QualibrationGraph.build( "long_graph", parameters=Parameters(qubits_1=[f"q{i}" for i in range(3)]), - orchestrator=BasicOrchestrator(skip_failed=True), -) as subg: - subg.add_node(library.nodes.get_nocopy("test_node").copy(name=f"node_1")) - for i in range(2,150): - subg.add_node(library.nodes.get_nocopy("test_node").copy(name=f"node_{i}")) - subg.connect(src=f"node_{i-1}", dst=f"node_{i}") +) as graph: + graph.add_node(library.nodes.get_nocopy("test_node").copy(name="node_1")) + for i in range(2, 150): + graph.add_node( + library.nodes.get_nocopy("test_node").copy(name=f"node_{i}") + ) + graph.connect(src=f"node_{i - 1}", dst=f"node_{i}") -subg.run() -# # node1 = library.nodes["test_node"] -# if __name__ == "__main__": -# print(45) \ No newline at end of file +if __name__ == "__main__": + a = graph.run() + print(a) diff --git a/tests/unit/test_qualibration_graph/test_graph.py b/tests/unit/test_qualibration_graph/test_graph.py index 9cfa278..a349685 100644 --- a/tests/unit/test_qualibration_graph/test_graph.py +++ b/tests/unit/test_qualibration_graph/test_graph.py @@ -666,20 +666,21 @@ def test__validate_graph_acyclic_without_cycle( ) graph._validate_graph_acyclic() + @pytest.mark.skip def test_connect_then_connect_on_failure_same_edge_overwrites( - self, pre_setup_graph_init, mock_library + self, pre_setup_graph_init, mock_library ): """Test that calling connect() then connect_on_failure() on the same edge overwrites to FAILED scenario""" (nodes, _, _, _) = pre_setup_graph_init with QualibrationGraph.build( - name="test_graph", - parameters=GraphParameters(), + name="test_graph", + parameters=GraphParameters(), ) as graph: graph.add_nodes(nodes["node1"], nodes["node2"]) - graph.connect_on_failure("node1", "node2") # Then: FAILED graph.connect("node1", "node2") # First: SUCCESS + graph.connect_on_failure("node1", "node2") # Then: FAILED # Should only have one edge with FAILED outcome (last write wins) assert len(graph._connectivity) == 1 diff --git a/tests/utils/local_qualibration_library.py b/tests/utils/local_qualibration_library.py index 878ff1b..f610542 100644 --- a/tests/utils/local_qualibration_library.py +++ b/tests/utils/local_qualibration_library.py @@ -1,13 +1,14 @@ -from unittest.mock import patch from pathlib import Path +from unittest.mock import patch + from qualibrate.qualibration_library import QualibrationLibrary + def load_local_qualibration_library(config): """ Creates a QualibrationLibrary with get_qualibrate_config and get_qualibrate_config_path patched to your desired values. """ - from qualibrate import qualibration_node ctx1 = patch("qualibrate.qualibration_node.get_qualibrate_config_path") ctx2 = patch( @@ -20,7 +21,10 @@ class PatchedLibraryContext: def __enter__(self): self._first_enter_result = ctx1.__enter__() self._second_enter_result = ctx2.__enter__() - cal_path = Path(__file__).parent / "calibration_nodes_and_graphs_for_testing" + cal_path = ( + Path(__file__).parent + / "calibration_nodes_and_graphs_for_testing" + ) self.lib = QualibrationLibrary(cal_path) return self.lib @@ -28,4 +32,4 @@ def __exit__(self, exc_type, exc, tb): ctx1.__exit__(exc_type, exc, tb) ctx2.__exit__(exc_type, exc, tb) - return PatchedLibraryContext() \ No newline at end of file + return PatchedLibraryContext() From 87e9fff0662d93b7b0d0a0426ed2b97a2c1bc196 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Wed, 3 Dec 2025 20:48:33 +0200 Subject: [PATCH 03/73] removed unnecessary module --- tests/utils/local_qualibration_library.py | 35 ----------------------- 1 file changed, 35 deletions(-) delete mode 100644 tests/utils/local_qualibration_library.py diff --git a/tests/utils/local_qualibration_library.py b/tests/utils/local_qualibration_library.py deleted file mode 100644 index f610542..0000000 --- a/tests/utils/local_qualibration_library.py +++ /dev/null @@ -1,35 +0,0 @@ -from pathlib import Path -from unittest.mock import patch - -from qualibrate.qualibration_library import QualibrationLibrary - - -def load_local_qualibration_library(config): - """ - Creates a QualibrationLibrary with get_qualibrate_config and - get_qualibrate_config_path patched to your desired values. - """ - - ctx1 = patch("qualibrate.qualibration_node.get_qualibrate_config_path") - ctx2 = patch( - "qualibrate.qualibration_node.get_qualibrate_config", - return_value=config, - ) - - # Return a context manager that patches AND yields the library - class PatchedLibraryContext: - def __enter__(self): - self._first_enter_result = ctx1.__enter__() - self._second_enter_result = ctx2.__enter__() - cal_path = ( - Path(__file__).parent - / "calibration_nodes_and_graphs_for_testing" - ) - self.lib = QualibrationLibrary(cal_path) - return self.lib - - def __exit__(self, exc_type, exc, tb): - ctx1.__exit__(exc_type, exc, tb) - ctx2.__exit__(exc_type, exc, tb) - - return PatchedLibraryContext() From c054df9da05aa79d19cc8fc00b11cdfe7e97803d Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Thu, 4 Dec 2025 09:17:03 +0200 Subject: [PATCH 04/73] ready for pr --- .../graph_with_loop.py | 4 ++++ .../graph_with_loop_condition_function.py | 4 ++++ .../graph_with_loop_condition_generatior.py | 4 ++++ .../graph_with_loop_condition_lambda.py | 4 ++++ .../graph_with_old_building_method.py | 6 +++++- .../graph_with_one_nested_graph.py | 4 ++++ .../graph_with_two_nested_graphs.py | 4 ++++ .../calibration_nodes_and_graphs_for_testing/long_graph.py | 4 ++-- 8 files changed, 31 insertions(+), 3 deletions(-) diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop.py index 54ae7dd..90f9b5b 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop.py +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop.py @@ -13,3 +13,7 @@ max_iterations=10, ) graph.connect("node", "node2") + +if __name__ == "__main__": + result = graph.run() + print(result) diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_function.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_function.py index a74cdb9..24955dc 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_function.py +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_function.py @@ -18,3 +18,7 @@ def check_fidelity(node: QualibrationNode, target: str): on=check_fidelity, ) graph.connect("node", "node2") + +if __name__ == "__main__": + result = graph.run() + print(result) \ No newline at end of file diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_generatior.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_generatior.py index 6ba66db..ecff1da 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_generatior.py +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_generatior.py @@ -20,3 +20,7 @@ def check_fidelity(node: QualibrationNode, target: str): max_iterations=10, ) graph.connect("node", "node2") + +if __name__ == "__main__": + result = graph.run() + print(result) \ No newline at end of file diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py index 0f23390..a59b343 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py @@ -13,3 +13,7 @@ on=lambda node, target: node.results[target]["fidelity"] < 0.95, ) graph.connect("node", "node2") + +if __name__ == "__main__": + result = graph.run() + print(result) \ No newline at end of file diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_old_building_method.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_old_building_method.py index b84f568..0647e64 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/graph_with_old_building_method.py +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_old_building_method.py @@ -14,9 +14,13 @@ class Parameters(GraphParameters): "node3": library.nodes.get_nocopy(USED_NODE).copy(name="node3"), } -g = QualibrationGraph( +graph = QualibrationGraph( name="graph_with_old_building_method", parameters=Parameters(qubits=[]), nodes=nodes, connectivity=[("node1", "node2")], ) + +if __name__ == "__main__": + result = graph.run() + print(result) diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_one_nested_graph.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_one_nested_graph.py index 8a792df..d3d6f38 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/graph_with_one_nested_graph.py +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_one_nested_graph.py @@ -28,3 +28,7 @@ class Parameters(GraphParameters): graph.add_node(library.nodes.get_nocopy("test_node").copy(name="node_4")) graph.connect(src="node_1", dst=subg) graph.connect(src=subg, dst="node_4") + +if __name__ == "__main__": + result = graph.run() + print(result) \ No newline at end of file diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_two_nested_graphs.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_two_nested_graphs.py index 1ab9c1d..46f3641 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/graph_with_two_nested_graphs.py +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_two_nested_graphs.py @@ -47,3 +47,7 @@ class Parameters(GraphParameters): graph.add_node(subg2) graph.connect(src="node_1", dst=subg) graph.connect(src=subg, dst=subg2) + +if __name__ == "__main__": + result = graph.run() + print(result) \ No newline at end of file diff --git a/tests/calibration_nodes_and_graphs_for_testing/long_graph.py b/tests/calibration_nodes_and_graphs_for_testing/long_graph.py index 41bff3a..fd35f04 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/long_graph.py +++ b/tests/calibration_nodes_and_graphs_for_testing/long_graph.py @@ -22,5 +22,5 @@ class Parameters(GraphParameters): graph.connect(src=f"node_{i - 1}", dst=f"node_{i}") if __name__ == "__main__": - a = graph.run() - print(a) + result = graph.run() + print(result) From e827000e7ca5f8c2ad59ac51031fc8db64ffac1f Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Thu, 4 Dec 2025 09:18:07 +0200 Subject: [PATCH 05/73] ready for pr --- tests/calibration_nodes_and_graphs_for_testing/epmty_node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/calibration_nodes_and_graphs_for_testing/epmty_node.py b/tests/calibration_nodes_and_graphs_for_testing/epmty_node.py index 88b780a..b46f473 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/epmty_node.py +++ b/tests/calibration_nodes_and_graphs_for_testing/epmty_node.py @@ -15,5 +15,5 @@ class Parameters(NodeParameters): @node.run_action() -def something(node: QualibrationNode): +def node_runs_indication(node: QualibrationNode): node.log("node is running") From 566f7f15baca8f94e6485ebb0c4b172f3a0feffd Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Thu, 4 Dec 2025 09:18:28 +0200 Subject: [PATCH 06/73] ready for pr --- .../{epmty_node.py => test_node.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/calibration_nodes_and_graphs_for_testing/{epmty_node.py => test_node.py} (100%) diff --git a/tests/calibration_nodes_and_graphs_for_testing/epmty_node.py b/tests/calibration_nodes_and_graphs_for_testing/test_node.py similarity index 100% rename from tests/calibration_nodes_and_graphs_for_testing/epmty_node.py rename to tests/calibration_nodes_and_graphs_for_testing/test_node.py From 70d2474b9093671b3cd89f2fa310d826df3f39cd Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Thu, 4 Dec 2025 09:22:55 +0200 Subject: [PATCH 07/73] ready for pr --- .../graph_with_loop_condition_function.py | 2 +- .../graph_with_loop_condition_generatior.py | 2 +- .../graph_with_loop_condition_lambda.py | 2 +- .../graph_with_one_nested_graph.py | 6 +++--- .../graph_with_two_nested_graphs.py | 10 +++++----- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_function.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_function.py index 24955dc..1ec1e6f 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_function.py +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_function.py @@ -21,4 +21,4 @@ def check_fidelity(node: QualibrationNode, target: str): if __name__ == "__main__": result = graph.run() - print(result) \ No newline at end of file + print(result) diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_generatior.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_generatior.py index ecff1da..f1839c8 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_generatior.py +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_generatior.py @@ -23,4 +23,4 @@ def check_fidelity(node: QualibrationNode, target: str): if __name__ == "__main__": result = graph.run() - print(result) \ No newline at end of file + print(result) diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py index a59b343..a5ce72e 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py @@ -16,4 +16,4 @@ if __name__ == "__main__": result = graph.run() - print(result) \ No newline at end of file + print(result) diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_one_nested_graph.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_one_nested_graph.py index d3d6f38..5c9d2a6 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/graph_with_one_nested_graph.py +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_one_nested_graph.py @@ -14,9 +14,9 @@ class Parameters(GraphParameters): name="graph_with_one_nested_graph", parameters=Parameters(qubits_1=["q1"]), ) as graph: - # create first node + # Create first node graph.add_node(library.nodes.get_nocopy("test_node").copy(name="node_1")) - # create first subgraph + # Create first subgraph with QualibrationGraph.build( name="subg", parameters=Parameters(qubits_1="q2"), @@ -31,4 +31,4 @@ class Parameters(GraphParameters): if __name__ == "__main__": result = graph.run() - print(result) \ No newline at end of file + print(result) diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_two_nested_graphs.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_two_nested_graphs.py index 46f3641..092329b 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/graph_with_two_nested_graphs.py +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_two_nested_graphs.py @@ -14,11 +14,11 @@ class Parameters(GraphParameters): name="graph_with_two_nested_graphs", parameters=Parameters(qubits_1=["q1"]), ) as graph: - # create first node + # Create first node graph.add_node(library.nodes.get_nocopy("test_node").copy(name="node_1")) - # create first subgraph + # Create first subgraph with QualibrationGraph.build( name="subg", @@ -29,7 +29,7 @@ class Parameters(GraphParameters): subg.connect(src="node_2", dst="node_3") graph.add_node(subg) - # second subgraph + # Create second subgraph with QualibrationGraph.build( name="subg2", @@ -43,11 +43,11 @@ class Parameters(GraphParameters): ) subg2.connect(src="node_4", dst="node_5") - # doing connections + # Connecting elements graph.add_node(subg2) graph.connect(src="node_1", dst=subg) graph.connect(src=subg, dst=subg2) if __name__ == "__main__": result = graph.run() - print(result) \ No newline at end of file + print(result) From fe05d41aec222eb89ef1874d88b27ffd0874d4b8 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Thu, 4 Dec 2025 11:25:01 +0200 Subject: [PATCH 08/73] check wether it what fails the ci --- .../test_qualibration_graph/test_graph.py | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/unit/test_qualibration_graph/test_graph.py b/tests/unit/test_qualibration_graph/test_graph.py index a349685..5b2803e 100644 --- a/tests/unit/test_qualibration_graph/test_graph.py +++ b/tests/unit/test_qualibration_graph/test_graph.py @@ -666,22 +666,22 @@ def test__validate_graph_acyclic_without_cycle( ) graph._validate_graph_acyclic() - @pytest.mark.skip - def test_connect_then_connect_on_failure_same_edge_overwrites( - self, pre_setup_graph_init, mock_library - ): - """Test that calling connect() then connect_on_failure() - on the same edge overwrites to FAILED scenario""" - (nodes, _, _, _) = pre_setup_graph_init - - with QualibrationGraph.build( - name="test_graph", - parameters=GraphParameters(), - ) as graph: - graph.add_nodes(nodes["node1"], nodes["node2"]) - graph.connect("node1", "node2") # First: SUCCESS - graph.connect_on_failure("node1", "node2") # Then: FAILED - - # Should only have one edge with FAILED outcome (last write wins) - assert len(graph._connectivity) == 1 - assert graph._connectivity[("node1", "node2")] == Outcome.FAILED + # @pytest.mark.skip + # def test_connect_then_connect_on_failure_same_edge_overwrites( + # self, pre_setup_graph_init, mock_library + # ): + # """Test that calling connect() then connect_on_failure() + # on the same edge overwrites to FAILED scenario""" + # (nodes, _, _, _) = pre_setup_graph_init + # + # with QualibrationGraph.build( + # name="test_graph", + # parameters=GraphParameters(), + # ) as graph: + # graph.add_nodes(nodes["node1"], nodes["node2"]) + # graph.connect("node1", "node2") # First: SUCCESS + # graph.connect_on_failure("node1", "node2") # Then: FAILED + # + # # Should only have one edge with FAILED outcome (last write wins) + # assert len(graph._connectivity) == 1 + # assert graph._connectivity[("node1", "node2")] == Outcome.FAILED From 3e0c6e92930ed08cea5327fb7da98a49d950c8ea Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Thu, 4 Dec 2025 11:27:33 +0200 Subject: [PATCH 09/73] pr --- .../test_qualibration_graph/test_graph.py | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/unit/test_qualibration_graph/test_graph.py b/tests/unit/test_qualibration_graph/test_graph.py index 5b2803e..a349685 100644 --- a/tests/unit/test_qualibration_graph/test_graph.py +++ b/tests/unit/test_qualibration_graph/test_graph.py @@ -666,22 +666,22 @@ def test__validate_graph_acyclic_without_cycle( ) graph._validate_graph_acyclic() - # @pytest.mark.skip - # def test_connect_then_connect_on_failure_same_edge_overwrites( - # self, pre_setup_graph_init, mock_library - # ): - # """Test that calling connect() then connect_on_failure() - # on the same edge overwrites to FAILED scenario""" - # (nodes, _, _, _) = pre_setup_graph_init - # - # with QualibrationGraph.build( - # name="test_graph", - # parameters=GraphParameters(), - # ) as graph: - # graph.add_nodes(nodes["node1"], nodes["node2"]) - # graph.connect("node1", "node2") # First: SUCCESS - # graph.connect_on_failure("node1", "node2") # Then: FAILED - # - # # Should only have one edge with FAILED outcome (last write wins) - # assert len(graph._connectivity) == 1 - # assert graph._connectivity[("node1", "node2")] == Outcome.FAILED + @pytest.mark.skip + def test_connect_then_connect_on_failure_same_edge_overwrites( + self, pre_setup_graph_init, mock_library + ): + """Test that calling connect() then connect_on_failure() + on the same edge overwrites to FAILED scenario""" + (nodes, _, _, _) = pre_setup_graph_init + + with QualibrationGraph.build( + name="test_graph", + parameters=GraphParameters(), + ) as graph: + graph.add_nodes(nodes["node1"], nodes["node2"]) + graph.connect("node1", "node2") # First: SUCCESS + graph.connect_on_failure("node1", "node2") # Then: FAILED + + # Should only have one edge with FAILED outcome (last write wins) + assert len(graph._connectivity) == 1 + assert graph._connectivity[("node1", "node2")] == Outcome.FAILED From 015ec678e6cd346443f95675433f13a97dfd6023 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Thu, 4 Dec 2025 11:37:50 +0200 Subject: [PATCH 10/73] pr --- .../{test_node.py => empty_node.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/calibration_nodes_and_graphs_for_testing/{test_node.py => empty_node.py} (100%) diff --git a/tests/calibration_nodes_and_graphs_for_testing/test_node.py b/tests/calibration_nodes_and_graphs_for_testing/empty_node.py similarity index 100% rename from tests/calibration_nodes_and_graphs_for_testing/test_node.py rename to tests/calibration_nodes_and_graphs_for_testing/empty_node.py From 65d2594d9177915ba95e64aa572e91c46e9e0345 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Thu, 4 Dec 2025 11:40:57 +0200 Subject: [PATCH 11/73] changed file names --- .../{empty_node2.py => demo_node.py} | 7 ++++++- .../calibration_nodes_and_graphs_for_testing/empty_node.py | 7 +------ 2 files changed, 7 insertions(+), 7 deletions(-) rename tests/calibration_nodes_and_graphs_for_testing/{empty_node2.py => demo_node.py} (63%) diff --git a/tests/calibration_nodes_and_graphs_for_testing/empty_node2.py b/tests/calibration_nodes_and_graphs_for_testing/demo_node.py similarity index 63% rename from tests/calibration_nodes_and_graphs_for_testing/empty_node2.py rename to tests/calibration_nodes_and_graphs_for_testing/demo_node.py index 03a52ac..b46f473 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/empty_node2.py +++ b/tests/calibration_nodes_and_graphs_for_testing/demo_node.py @@ -10,5 +10,10 @@ class Parameters(NodeParameters): float_value: float = 1.0 -node = QualibrationNode("test_node2", parameters=Parameters()) +node = QualibrationNode("test_node", parameters=Parameters()) node.parameters = Parameters() + + +@node.run_action() +def node_runs_indication(node: QualibrationNode): + node.log("node is running") diff --git a/tests/calibration_nodes_and_graphs_for_testing/empty_node.py b/tests/calibration_nodes_and_graphs_for_testing/empty_node.py index b46f473..03a52ac 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/empty_node.py +++ b/tests/calibration_nodes_and_graphs_for_testing/empty_node.py @@ -10,10 +10,5 @@ class Parameters(NodeParameters): float_value: float = 1.0 -node = QualibrationNode("test_node", parameters=Parameters()) +node = QualibrationNode("test_node2", parameters=Parameters()) node.parameters = Parameters() - - -@node.run_action() -def node_runs_indication(node: QualibrationNode): - node.log("node is running") From 9d9bb7741cd087342d621897efebeae2e86788d0 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Sun, 7 Dec 2025 15:27:00 +0200 Subject: [PATCH 12/73] finished adapting to operational operation concept, also started implementing connect on failure condition on graph, need to adaps orchestrator to connec on failure with condition --- qualibrate/models/operationalCondition.py | 22 +++++ .../orchestration/basic_orchestrator.py | 7 +- qualibrate/qualibration_graph.py | 84 ++++++++++++------- qualibrate/utils/graph_building.py | 1 + 4 files changed, 83 insertions(+), 31 deletions(-) create mode 100644 qualibrate/models/operationalCondition.py diff --git a/qualibrate/models/operationalCondition.py b/qualibrate/models/operationalCondition.py new file mode 100644 index 0000000..280c3c8 --- /dev/null +++ b/qualibrate/models/operationalCondition.py @@ -0,0 +1,22 @@ +from qualibrate.models.outcome import Outcome +from qualibrate.utils.graph_building import ( + GraphElementTypeVar) +from pydantic import BaseModel +from typing import Generic +from collections.abc import Generator, Callable +from qualibrate.utils.type_protocols import TargetType + + +class OperationalCondition(BaseModel, Generic[GraphElementTypeVar]): + # on_failure: bool = False + on_scenario : Outcome + on_function: Callable[[GraphElementTypeVar, TargetType], bool] | None = None + on_generator: ( + Callable[ + [], + Generator[ + bool, tuple[GraphElementTypeVar, TargetType] | None, None + ], + ] + | None + ) = None diff --git a/qualibrate/orchestration/basic_orchestrator.py b/qualibrate/orchestration/basic_orchestrator.py index e3e4fc3..c95e00b 100644 --- a/qualibrate/orchestration/basic_orchestrator.py +++ b/qualibrate/orchestration/basic_orchestrator.py @@ -211,8 +211,9 @@ def _set_out_targets_for_element( f"Can't set out targets of {element} without run summary" ) + # self.nx_graph.edges[element, successor]["operational_condition"] is of type OperationalCondition has_on_failed_successors = any( - self.nx_graph.edges[element, successor]["scenario"] + self.nx_graph.edges[element, successor]["operational_condition"].on_scenario == Outcome.FAILED for successor in self.nx_graph.successors(element) ) @@ -225,7 +226,7 @@ def _set_out_targets_for_element( QualibrationGraph.EDGE_TARGETS_FIELD ] = ( successful_out_targets - if self.nx_graph.edges[element, successor]["scenario"] + if self.nx_graph.edges[element, successor]["operational_condition"].on_scenario == Outcome.SUCCESSFUL else failed_out_targets ) @@ -285,7 +286,7 @@ def _is_loop_iteration_needed( yield False return if ( - conditions.on_failure + conditions.on_scenario == Outcome.FAILED and nx_graph.nodes[element_to_run][ QualibrationGraph.ELEMENT_STATUS_FIELD ] diff --git a/qualibrate/qualibration_graph.py b/qualibrate/qualibration_graph.py index 5e6c486..227913c 100644 --- a/qualibrate/qualibration_graph.py +++ b/qualibrate/qualibration_graph.py @@ -21,6 +21,7 @@ from qualibrate.models.node_status import ElementRunStatus from qualibrate.models.outcome import Outcome +from qualibrate.models.operationalCondition import OperationalCondition from qualibrate.models.run_mode import RunModes from qualibrate.models.run_summary.base import BaseRunSummary from qualibrate.models.run_summary.graph import GraphRunSummary @@ -70,23 +71,24 @@ GraphElLibT = QRunnable[RunnableParameters, RunnableParameters] -class LoopCondition(BaseModel, Generic[GraphElementTypeVar]): - on_failure: bool = False - on_function: Callable[[GraphElementTypeVar, TargetType], bool] | None = None - on_generator: ( - Callable[ - [], - Generator[ - bool, tuple[GraphElementTypeVar, TargetType] | None, None - ], - ] - | None - ) = None +# class LoopCondition(BaseModel, Generic[GraphElementTypeVar]): +# on_failure: bool = False +# on_function: Callable[[GraphElementTypeVar, TargetType], bool] | None = None +# on_generator: ( +# Callable[ +# [], +# Generator[ +# bool, tuple[GraphElementTypeVar, TargetType] | None, None +# ], +# ] +# | None +# ) = None +# max_iterations: int | None = None +# """ +# Common amount for all loop types (on failure, on function, on generator) +# """ +class LoopCondition(OperationalCondition[GraphElementTypeVar]): max_iterations: int | None = None - """ - Common amount for all loop types (on failure, on function, on generator) - """ - class QualibrationGraph( QRunnable[GraphCreateParametersType, GraphRunParametersType], @@ -144,7 +146,7 @@ def __init__( if not isinstance(parameters, GraphParameters): raise ValueError("Graph parameters must be of type GraphParameters") super().__init__(name, parameters, description=description, modes=modes) - self._connectivity: dict[tuple[str, str], Outcome] + self._connectivity: dict[tuple[str, str], OperationalCondition[GraphElementTypeVar]] if finalize: if nodes is None or connectivity is None: raise RuntimeError( @@ -153,8 +155,9 @@ def __init__( ) self._elements = dict(nodes) self._connectivity = { - connectivity: Outcome.SUCCESSFUL - for connectivity in connectivity + # connectivity: Outcome.SUCCESSFUL + connectivity: OperationalCondition[GraphElementTypeVar](on_scenario=Outcome.SUCCESSFUL) + for connectivity in connectivity } else: self._elements = {} @@ -265,7 +268,7 @@ def __copy__(self) -> Self: new_graph._graph.add_edge( new_graph._elements[source], new_graph._elements[destination], - scenario=self._connectivity[(source, destination)], + operational_condition=copy.deepcopy(self._connectivity[(source, destination)]), ) # Copy orchestrator if it exists @@ -347,7 +350,7 @@ def _add_nodes_and_connections_to_nx(self) -> None: self._graph.add_edge( source_element, destination_element, - scenario=self._connectivity[(source, destination)], + operational_condition=self._connectivity[(source, destination)], ) self._validate_graph_acyclic() @@ -1088,7 +1091,7 @@ def connect( src: str | GraphElementTypeVar, dst: str | GraphElementTypeVar, ) -> None: - self._connect(src, dst, Outcome.SUCCESSFUL) + self._connect(src, dst) @ensure_not_finalized @ensure_building @@ -1097,8 +1100,30 @@ def connect_on_failure( /, src: str | GraphElementTypeVar, dst: str | GraphElementTypeVar, + element: str | GraphElementTypeVar, + on: ( + Callable[ + [], + Generator[ + bool, tuple[GraphElementTypeVar, TargetType] | None, None + ], + ] + | Callable[[GraphElementTypeVar, TargetType], bool] + | None + ) = None, + ) -> None: - self._connect(src, dst, Outcome.FAILED) + + on_function, on_generator = None, None + if on is not None: + if isgeneratorfunction(on): + on_generator = on + else: + on_function = cast( + Callable[[GraphElementTypeVar, TargetType], bool], on + ) + operational_condition = OperationalCondition(on_function = on_function, on_generator = on_generator, on_scenario=Outcome.FAILED) + self._connect(src, dst, operational_condition) @ensure_not_finalized @ensure_building @@ -1106,7 +1131,8 @@ def _connect( self, src: str | GraphElementTypeVar, dst: str | GraphElementTypeVar, - run_scenario: Outcome, + # run_scenario: Outcome, + operational_condition: OperationalCondition[GraphElementTypeVar] = None, ) -> None: s = self._resolve_element_name(src) d = self._resolve_element_name(dst) @@ -1115,9 +1141,11 @@ def _connect( f"Both '{s}' and '{d}' must be added before connecting." ) edge = (s, d) - if edge in self._connectivity: - return - self._connectivity[edge] = run_scenario + self._connectivity[edge] = operational_condition if operational_condition else OperationalCondition(on_scenario=Outcome.SUCCESSFUL) + + # if edge in self._connectivity: + # return + # self._connectivity[edge] = run_scenario def _get_validated_element_name( self, element: str | GraphElementTypeVar @@ -1168,4 +1196,4 @@ def _loop_on_failure( element_name = self._get_validated_element_name(element) conditions = self._loop_conditions[element_name] conditions.max_iterations = max_iterations - conditions.on_failure = True + conditions.on_scenario = Outcome.FAILED diff --git a/qualibrate/utils/graph_building.py b/qualibrate/utils/graph_building.py index 7b91ee0..562e950 100644 --- a/qualibrate/utils/graph_building.py +++ b/qualibrate/utils/graph_building.py @@ -27,6 +27,7 @@ R = TypeVar("R") + def ensure_finalized( fn: Callable[Concatenate["QualibrationGraph[Any]", P], R], ) -> Callable[Concatenate["QualibrationGraph[Any]", P], R]: From 140a3a5ae972a78988a9306f9b89a0e248c6bc2b Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Wed, 10 Dec 2025 17:54:24 +0200 Subject: [PATCH 13/73] connect on failure condition and conditional operation should work, need to add tests --- ...onalCondition.py => operational_condition.py} | 0 qualibrate/orchestration/basic_orchestrator.py | 16 +++++++++++++--- qualibrate/qualibration_graph.py | 4 ++-- 3 files changed, 15 insertions(+), 5 deletions(-) rename qualibrate/models/{operationalCondition.py => operational_condition.py} (100%) diff --git a/qualibrate/models/operationalCondition.py b/qualibrate/models/operational_condition.py similarity index 100% rename from qualibrate/models/operationalCondition.py rename to qualibrate/models/operational_condition.py diff --git a/qualibrate/orchestration/basic_orchestrator.py b/qualibrate/orchestration/basic_orchestrator.py index c95e00b..a04354d 100644 --- a/qualibrate/orchestration/basic_orchestrator.py +++ b/qualibrate/orchestration/basic_orchestrator.py @@ -4,9 +4,8 @@ from datetime import datetime from queue import Queue from typing import Generic, cast - +from qualibrate.models.operational_condition import OperationalCondition import networkx as nx - from qualibrate.models.execution_history import ( ExecutionHistoryItem, ItemData, @@ -228,7 +227,8 @@ def _set_out_targets_for_element( successful_out_targets if self.nx_graph.edges[element, successor]["operational_condition"].on_scenario == Outcome.SUCCESSFUL - else failed_out_targets + else self._execute_condition(self.nx_graph.edges[element, successor]["operational_condition"], + element, failed_out_targets) ) else: successful_out_targets = ( @@ -242,6 +242,16 @@ def _set_out_targets_for_element( QualibrationGraph.EDGE_TARGETS_FIELD ] = successful_out_targets + def _execute_condition(self,conditional_operation: OperationalCondition[GraphElementTypeVar], element: GraphElementTypeVar, targets: list[TargetType]) ->list[TargetType]: + if conditional_operation.on_generator is not None: + executed_condition = conditional_operation.on_generator() + #priming the generator, we need to get to the point where the generator expects out two variables + executed_condition.send(None) + return [target for target in targets if executed_condition.send((element, target))] + return [target for target in targets if conditional_operation.on_function(element, target)] + + + def _execute_loop_iteration( self, element_to_run: GraphElementTypeVar, diff --git a/qualibrate/qualibration_graph.py b/qualibrate/qualibration_graph.py index 227913c..1a6e912 100644 --- a/qualibrate/qualibration_graph.py +++ b/qualibrate/qualibration_graph.py @@ -21,7 +21,7 @@ from qualibrate.models.node_status import ElementRunStatus from qualibrate.models.outcome import Outcome -from qualibrate.models.operationalCondition import OperationalCondition +from qualibrate.models.operational_condition import OperationalCondition from qualibrate.models.run_mode import RunModes from qualibrate.models.run_summary.base import BaseRunSummary from qualibrate.models.run_summary.graph import GraphRunSummary @@ -864,7 +864,7 @@ def __serialize_graph_represantation_inner( node_conditions.on_generator is not None or node_conditions.on_function is not None ) - loop_data["on_failure"] = node_conditions.on_failure + loop_data["on_failure"] = node_conditions.on_scenario == Outcome.FAILED loop_data["max_iterations"] = node_conditions.max_iterations if isinstance(element, QualibrationGraph): From ad17b92eb871c6ea282d761a0045b8796a62b4b4 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Thu, 11 Dec 2025 11:34:14 +0200 Subject: [PATCH 14/73] tests for loop construct ready --- .../test_basic_orchestrator.py | 329 +++++++++++++++++- 1 file changed, 327 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_orchestration/test_basic_orchestrator.py b/tests/unit/test_orchestration/test_basic_orchestrator.py index 8f38e9b..4ad4e95 100644 --- a/tests/unit/test_orchestration/test_basic_orchestrator.py +++ b/tests/unit/test_orchestration/test_basic_orchestrator.py @@ -10,6 +10,7 @@ from qualibrate.orchestration.basic_orchestrator import BasicOrchestrator from qualibrate.parameters import RunnableParameters from qualibrate.qualibration_graph import QualibrationGraph +from qualibrate.qualibration_graph import LoopCondition class TestBasicOrchestrator: @@ -239,7 +240,78 @@ def test_get_in_targets_intersection_with_multiple_predecessors( # Should be intersection: q2 and q3 assert set(result) == {"q2", "q3"} - # TODO: get in targets with loop conditions + def test_get_in_targets_with_loop_targets(self,mocker): + """Test that _get_in_targets includes loop targets from previous iterations""" + orchestrator = BasicOrchestrator() + + mock_node = MagicMock() + mock_pred = MagicMock() + + mock_nx_graph = mocker.patch( + "qualibrate.orchestration.basic_orchestrator.BasicOrchestrator.nx_graph", + new_callable=mocker.PropertyMock, + ) + mock_nx_graph.return_value.predecessors.return_value = [mock_pred] + mock_nx_graph.return_value.edges = { + (mock_pred, mock_node): { + QualibrationGraph.EDGE_TARGETS_FIELD: ["q1", "q2"] + } + } + # Mock loop targets on the node itself + mock_nx_graph.return_value.nodes = { + mock_node: { + QualibrationGraph.LOOP_TARGETS_FIELD: ["q3", "q4"], + QualibrationGraph.ELEMENT_STATUS_FIELD: ElementRunStatus.pending + }, + mock_pred: { + QualibrationGraph.ELEMENT_STATUS_FIELD: ElementRunStatus.finished + } + } + + result = orchestrator._get_in_targets_for_element(mock_node) + + # Should include both edge targets (q1, q2) AND loop targets (q3, q4) + assert set(result) == {"q1", "q2", "q3", "q4"} + + def test_get_in_targets_with_loop_targets_and_multiple_predecessors(self, mocker): + """Test that loop targets are added to the intersection of predecessor targets""" + orchestrator = BasicOrchestrator() + + mock_node = MagicMock() + mock_pred1 = MagicMock() + mock_pred2 = MagicMock() + + mock_nx_graph = mocker.patch( + "qualibrate.orchestration.basic_orchestrator.BasicOrchestrator.nx_graph", + new_callable=mocker.PropertyMock, + ) + mock_nx_graph.return_value.predecessors.return_value = [mock_pred1, mock_pred2] + mock_nx_graph.return_value.edges = { + (mock_pred1, mock_node): { + QualibrationGraph.EDGE_TARGETS_FIELD: ["q1", "q2", "q3"] + }, + (mock_pred2, mock_node): { + QualibrationGraph.EDGE_TARGETS_FIELD: ["q2", "q3", "q4"] + }, + } + # Mock loop targets + mock_nx_graph.return_value.nodes = { + mock_node: { + QualibrationGraph.LOOP_TARGETS_FIELD: ["q5"], + QualibrationGraph.ELEMENT_STATUS_FIELD: ElementRunStatus.pending + }, + mock_pred1: { + QualibrationGraph.ELEMENT_STATUS_FIELD: ElementRunStatus.finished + }, + mock_pred2: { + QualibrationGraph.ELEMENT_STATUS_FIELD: ElementRunStatus.finished + } + } + + result = orchestrator._get_in_targets_for_element(mock_node) + + # Intersection of predecessors (q2, q3) + loop targets (q5) + assert set(result) == {"q2", "q3", "q5"} def test_set_out_targets_all_successful_skip_failed_disabled(self, mocker): """Test _set_out_targets with all successful targets @@ -436,7 +508,260 @@ def edges_getitem(key): "q2", ] - # TODO: test loop related logic + def test_is_loop_iteration_needed_no_conditions(self): + """Test that loop stops immediately when no loop conditions are defined""" + orchestrator = BasicOrchestrator() + mock_element = MagicMock() + mock_element.name = "test_element" + + mock_graph = MagicMock() + mock_graph._loop_conditions = {} + + orchestrator._graph = mock_graph + + iteration_generator = orchestrator._is_loop_iteration_needed(mock_element) + + # First yield should be True (initial execution) + assert next(iteration_generator) is True + # Second yield should be False (no loop conditions) + assert iteration_generator.send(None) is False + + def test_is_loop_iteration_needed_max_iterations(self): + """Test that loop respects max_iterations limit""" + orchestrator = BasicOrchestrator() + mock_element = MagicMock() + mock_element.name = "test_element" + mock_element.run_summary = None + + mock_graph = MagicMock() + mock_graph._loop_conditions = { + "test_element": LoopCondition(max_iterations=3) + } + + orchestrator._graph = mock_graph + + iteration_generator = orchestrator._is_loop_iteration_needed(mock_element) + + # First yield - initial execution + assert next(iteration_generator) is True + + # Iterations 1 and 2 should continue + assert iteration_generator.send(None) is True + assert iteration_generator.send(None) is True + + # Iteration 3 (max reached) should stop + assert iteration_generator.send(None) is False + + def test_is_loop_iteration_needed_on_function_with_reuse_targets(self, mocker): + """Test loop with on_function that filters targets for re-execution""" + orchestrator = BasicOrchestrator() + mock_element = MagicMock() + mock_element.name = "test_element" + + # Mock run summary with initial targets + mock_summary = MagicMock() + mock_summary.initial_targets = ["q1", "q2", "q3"] + mock_element.run_summary = mock_summary + + # Filter function: only reuse targets q1 and q2 + def filter_func(element, target): + return target in ["q1", "q2"] + + + mock_graph = MagicMock() + mock_graph._loop_conditions = { + "test_element": LoopCondition(on_function=filter_func, max_iterations=5) + } + + mock_nx_graph = mocker.patch( + "qualibrate.orchestration.basic_orchestrator.BasicOrchestrator.nx_graph", + new_callable=mocker.PropertyMock, + ) + mock_nx_graph.return_value.nodes = { + mock_element: { + QualibrationGraph.ELEMENT_STATUS_FIELD: ElementRunStatus.pending + } + } + + orchestrator._graph = mock_graph + + iteration_generator = orchestrator._is_loop_iteration_needed(mock_element) + + # Initial execution + assert next(iteration_generator) is True + + # After first run, should continue with q1, q2 + assert iteration_generator.send(None) is True + + # Check that loop targets were set + assert mock_nx_graph.return_value.nodes[mock_element][ + QualibrationGraph.LOOP_TARGETS_FIELD + ] == ["q1", "q2"] + + def test_is_loop_iteration_needed_on_function_no_reuse_targets(self): + """Test loop stops when on_function filters out all targets""" + orchestrator = BasicOrchestrator() + mock_element = MagicMock() + mock_element.name = "test_element" + + mock_summary = MagicMock() + mock_summary.initial_targets = ["q1", "q2", "q3"] + mock_element.run_summary = mock_summary + + # Filter function: reject all targets + def filter_func(element, target): + return False + + mock_graph = MagicMock() + mock_graph._loop_conditions = { + "test_element": LoopCondition(on_function=filter_func) + } + + orchestrator._graph = mock_graph + + iteration_generator = orchestrator._is_loop_iteration_needed(mock_element) + + # Initial execution + assert next(iteration_generator) is True + + # No targets to reuse, should stop + assert iteration_generator.send(None) is False + + def test_is_loop_iteration_needed_on_generator(self): + """Test loop with generator function that controls iteration""" + orchestrator = BasicOrchestrator() + mock_element = MagicMock() + mock_element.name = "test_element" + + mock_summary = MagicMock() + mock_summary.initial_targets = ["q1", "q2"] + mock_element.run_summary = mock_summary + + # Generator function: accept q1 once, then stop + def generator_func(): + count = 0 + while True: + element, target = yield + if target == "q1" and count < 1: + count += 1 + yield True + else: + yield False + + mock_graph = MagicMock() + mock_graph._loop_conditions = { + "test_element": LoopCondition(on_generator=generator_func) + } + + orchestrator._graph = mock_graph + + iteration_generator = orchestrator._is_loop_iteration_needed(mock_element) + + # Initial execution + assert next(iteration_generator) is True + assert iteration_generator.send(None) is True + assert iteration_generator.send(None) is False + + def test_is_loop_iteration_needed_on_failure(self, mocker): + """Test loop continues when node fails and on_failure is True""" + orchestrator = BasicOrchestrator() + mock_element = MagicMock() + mock_element.name = "test_element" + mock_element.run_summary = None + + mock_graph = MagicMock() + mock_graph._loop_conditions = { + "test_element": LoopCondition(on_failure=True, max_iterations=3) + } + + + orchestrator._graph = mock_graph + + iteration_generator = orchestrator._is_loop_iteration_needed(mock_element) + + # Initial execution + assert next(iteration_generator) is True + + # Node failed (error status), should retry + assert iteration_generator.send(None) is True + + # Should continue until max_iterations + assert iteration_generator.send(None) is True + + # Max iterations reached + assert iteration_generator.send(None) is False + + def test_traverse_graph_with_loop_condition(self): + """Integration test: Graph with a node that loops based on a condition""" + orchestrator = BasicOrchestrator() + + # Create mock node that "fails" first time, succeeds second time + node1 = MagicMock(spec=QualibrationNode) + node1.name = "looping_node" + node1.description = None + node1.parameters = MagicMock(spec=RunnableParameters) + node1.outcomes = {} + node1.snapshot_idx = None + + # Track execution count + execution_count = [0] + + # First run: some targets fail + summary1 = MagicMock(spec=NodeRunSummary) + summary1.successful_targets = ["q1"] + summary1.failed_targets = ["q2"] + summary1.initial_targets = ["q1", "q2"] + + # Second run: all succeed + summary2 = MagicMock(spec=NodeRunSummary) + summary2.successful_targets = ["q2"] + summary2.failed_targets = [] + summary2.initial_targets = ["q2"] + + def node_run(interactive=False, **kwargs): + execution_count[0] += 1 + if execution_count[0] == 1: + node1.run_summary = summary1 + return summary1 + else: + node1.run_summary = summary2 + return summary2 + + node1.run = node_run + + mock_graph = MagicMock(spec=QualibrationGraph) + mock_graph.name = "test_graph" + + # Loop condition: retry failed targets + def should_retry(element, target): + return target in element.run_summary.failed_targets + + mock_graph._loop_conditions = { + "looping_node": LoopCondition(on_function=should_retry, max_iterations=5) + } + + # Setup parameters + mock_params = MagicMock() + node1_params = MagicMock() + node1_params.targets = None + node1_params.model_dump.return_value = {} + mock_params.looping_node = node1_params + mock_graph.full_parameters.nodes = mock_params + + # Create NetworkX graph + import networkx as nx + nx_graph = nx.DiGraph() + nx_graph.add_node(node1, status=ElementRunStatus.pending, retries=0) + mock_graph._graph = nx_graph + + # Run traversal + orchestrator.traverse_graph(mock_graph, ["q1", "q2"]) + + # Node should have been executed twice (initial + 1 retry for q2) + assert execution_count[0] == 2 + + # Final status should be finished + assert nx_graph.nodes[node1][QualibrationGraph.ELEMENT_STATUS_FIELD] == ElementRunStatus.finished def test_traverse_graph_with_success_and_failure_paths(self, mocker): """ From b33d7f3eec4f884513f724a444a7e5d5949b294e Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Thu, 11 Dec 2025 12:08:27 +0200 Subject: [PATCH 15/73] linted code --- .../test_basic_orchestrator.py | 82 +++++++++++++------ 1 file changed, 55 insertions(+), 27 deletions(-) diff --git a/tests/unit/test_orchestration/test_basic_orchestrator.py b/tests/unit/test_orchestration/test_basic_orchestrator.py index 4ad4e95..2af668b 100644 --- a/tests/unit/test_orchestration/test_basic_orchestrator.py +++ b/tests/unit/test_orchestration/test_basic_orchestrator.py @@ -9,8 +9,7 @@ from qualibrate.models.run_summary.node import NodeRunSummary from qualibrate.orchestration.basic_orchestrator import BasicOrchestrator from qualibrate.parameters import RunnableParameters -from qualibrate.qualibration_graph import QualibrationGraph -from qualibrate.qualibration_graph import LoopCondition +from qualibrate.qualibration_graph import LoopCondition, QualibrationGraph class TestBasicOrchestrator: @@ -240,8 +239,9 @@ def test_get_in_targets_intersection_with_multiple_predecessors( # Should be intersection: q2 and q3 assert set(result) == {"q2", "q3"} - def test_get_in_targets_with_loop_targets(self,mocker): - """Test that _get_in_targets includes loop targets from previous iterations""" + def test_get_in_targets_with_loop_targets(self, mocker): + """Test that _get_in_targets includes + loop targets from previous iterations""" orchestrator = BasicOrchestrator() mock_node = MagicMock() @@ -261,11 +261,11 @@ def test_get_in_targets_with_loop_targets(self,mocker): mock_nx_graph.return_value.nodes = { mock_node: { QualibrationGraph.LOOP_TARGETS_FIELD: ["q3", "q4"], - QualibrationGraph.ELEMENT_STATUS_FIELD: ElementRunStatus.pending + QualibrationGraph.ELEMENT_STATUS_FIELD: ElementRunStatus.pending, }, mock_pred: { QualibrationGraph.ELEMENT_STATUS_FIELD: ElementRunStatus.finished - } + }, } result = orchestrator._get_in_targets_for_element(mock_node) @@ -273,8 +273,11 @@ def test_get_in_targets_with_loop_targets(self,mocker): # Should include both edge targets (q1, q2) AND loop targets (q3, q4) assert set(result) == {"q1", "q2", "q3", "q4"} - def test_get_in_targets_with_loop_targets_and_multiple_predecessors(self, mocker): - """Test that loop targets are added to the intersection of predecessor targets""" + def test_get_in_targets_with_loop_targets_and_multiple_predecessors( + self, mocker + ): + """Test that loop targets are added to the intersection + of predecessor targets""" orchestrator = BasicOrchestrator() mock_node = MagicMock() @@ -285,7 +288,10 @@ def test_get_in_targets_with_loop_targets_and_multiple_predecessors(self, mocker "qualibrate.orchestration.basic_orchestrator.BasicOrchestrator.nx_graph", new_callable=mocker.PropertyMock, ) - mock_nx_graph.return_value.predecessors.return_value = [mock_pred1, mock_pred2] + mock_nx_graph.return_value.predecessors.return_value = [ + mock_pred1, + mock_pred2, + ] mock_nx_graph.return_value.edges = { (mock_pred1, mock_node): { QualibrationGraph.EDGE_TARGETS_FIELD: ["q1", "q2", "q3"] @@ -298,14 +304,14 @@ def test_get_in_targets_with_loop_targets_and_multiple_predecessors(self, mocker mock_nx_graph.return_value.nodes = { mock_node: { QualibrationGraph.LOOP_TARGETS_FIELD: ["q5"], - QualibrationGraph.ELEMENT_STATUS_FIELD: ElementRunStatus.pending + QualibrationGraph.ELEMENT_STATUS_FIELD: ElementRunStatus.pending, }, mock_pred1: { QualibrationGraph.ELEMENT_STATUS_FIELD: ElementRunStatus.finished }, mock_pred2: { QualibrationGraph.ELEMENT_STATUS_FIELD: ElementRunStatus.finished - } + }, } result = orchestrator._get_in_targets_for_element(mock_node) @@ -509,7 +515,8 @@ def edges_getitem(key): ] def test_is_loop_iteration_needed_no_conditions(self): - """Test that loop stops immediately when no loop conditions are defined""" + """Test that loop stops immediately when + no loop conditions are defined""" orchestrator = BasicOrchestrator() mock_element = MagicMock() mock_element.name = "test_element" @@ -519,7 +526,9 @@ def test_is_loop_iteration_needed_no_conditions(self): orchestrator._graph = mock_graph - iteration_generator = orchestrator._is_loop_iteration_needed(mock_element) + iteration_generator = orchestrator._is_loop_iteration_needed( + mock_element + ) # First yield should be True (initial execution) assert next(iteration_generator) is True @@ -540,7 +549,9 @@ def test_is_loop_iteration_needed_max_iterations(self): orchestrator._graph = mock_graph - iteration_generator = orchestrator._is_loop_iteration_needed(mock_element) + iteration_generator = orchestrator._is_loop_iteration_needed( + mock_element + ) # First yield - initial execution assert next(iteration_generator) is True @@ -552,7 +563,9 @@ def test_is_loop_iteration_needed_max_iterations(self): # Iteration 3 (max reached) should stop assert iteration_generator.send(None) is False - def test_is_loop_iteration_needed_on_function_with_reuse_targets(self, mocker): + def test_is_loop_iteration_needed_on_function_with_reuse_targets( + self, mocker + ): """Test loop with on_function that filters targets for re-execution""" orchestrator = BasicOrchestrator() mock_element = MagicMock() @@ -567,10 +580,11 @@ def test_is_loop_iteration_needed_on_function_with_reuse_targets(self, mocker): def filter_func(element, target): return target in ["q1", "q2"] - mock_graph = MagicMock() mock_graph._loop_conditions = { - "test_element": LoopCondition(on_function=filter_func, max_iterations=5) + "test_element": LoopCondition( + on_function=filter_func, max_iterations=5 + ) } mock_nx_graph = mocker.patch( @@ -585,7 +599,9 @@ def filter_func(element, target): orchestrator._graph = mock_graph - iteration_generator = orchestrator._is_loop_iteration_needed(mock_element) + iteration_generator = orchestrator._is_loop_iteration_needed( + mock_element + ) # Initial execution assert next(iteration_generator) is True @@ -595,8 +611,8 @@ def filter_func(element, target): # Check that loop targets were set assert mock_nx_graph.return_value.nodes[mock_element][ - QualibrationGraph.LOOP_TARGETS_FIELD - ] == ["q1", "q2"] + QualibrationGraph.LOOP_TARGETS_FIELD + ] == ["q1", "q2"] def test_is_loop_iteration_needed_on_function_no_reuse_targets(self): """Test loop stops when on_function filters out all targets""" @@ -619,7 +635,9 @@ def filter_func(element, target): orchestrator._graph = mock_graph - iteration_generator = orchestrator._is_loop_iteration_needed(mock_element) + iteration_generator = orchestrator._is_loop_iteration_needed( + mock_element + ) # Initial execution assert next(iteration_generator) is True @@ -655,7 +673,9 @@ def generator_func(): orchestrator._graph = mock_graph - iteration_generator = orchestrator._is_loop_iteration_needed(mock_element) + iteration_generator = orchestrator._is_loop_iteration_needed( + mock_element + ) # Initial execution assert next(iteration_generator) is True @@ -674,10 +694,11 @@ def test_is_loop_iteration_needed_on_failure(self, mocker): "test_element": LoopCondition(on_failure=True, max_iterations=3) } - orchestrator._graph = mock_graph - iteration_generator = orchestrator._is_loop_iteration_needed(mock_element) + iteration_generator = orchestrator._is_loop_iteration_needed( + mock_element + ) # Initial execution assert next(iteration_generator) is True @@ -692,7 +713,8 @@ def test_is_loop_iteration_needed_on_failure(self, mocker): assert iteration_generator.send(None) is False def test_traverse_graph_with_loop_condition(self): - """Integration test: Graph with a node that loops based on a condition""" + """Integration test: Graph with a node + that loops based on a condition""" orchestrator = BasicOrchestrator() # Create mock node that "fails" first time, succeeds second time @@ -737,7 +759,9 @@ def should_retry(element, target): return target in element.run_summary.failed_targets mock_graph._loop_conditions = { - "looping_node": LoopCondition(on_function=should_retry, max_iterations=5) + "looping_node": LoopCondition( + on_function=should_retry, max_iterations=5 + ) } # Setup parameters @@ -750,6 +774,7 @@ def should_retry(element, target): # Create NetworkX graph import networkx as nx + nx_graph = nx.DiGraph() nx_graph.add_node(node1, status=ElementRunStatus.pending, retries=0) mock_graph._graph = nx_graph @@ -761,7 +786,10 @@ def should_retry(element, target): assert execution_count[0] == 2 # Final status should be finished - assert nx_graph.nodes[node1][QualibrationGraph.ELEMENT_STATUS_FIELD] == ElementRunStatus.finished + assert ( + nx_graph.nodes[node1][QualibrationGraph.ELEMENT_STATUS_FIELD] + == ElementRunStatus.finished + ) def test_traverse_graph_with_success_and_failure_paths(self, mocker): """ From fe8b7c64507edb2119b7cf57849a57b36d7ed97b Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Thu, 11 Dec 2025 13:31:49 +0200 Subject: [PATCH 16/73] linted code --- pyproject.toml | 9 +++++++-- qualibrate/storage/local_storage_manager.py | 7 +++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0ff8119..3860e29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,10 +51,15 @@ select = [ "SIM", # flake8-simplify "I", # isort ] +#ignore = [ +# "E501" # ignore line-too-long errors +##] +#[tool.ruff.lint.per-file-ignores] +#"*.py" = ["E501"] # E501 will be ignored for exit code, but still displayed [tool.ruff.lint.pycodestyle] -max-line-length = 80 -max-doc-length = 80 +max-line-length = 100 +max-doc-length = 100 [tool.mypy] python_version = "3.10" diff --git a/qualibrate/storage/local_storage_manager.py b/qualibrate/storage/local_storage_manager.py index 96582a8..2dbee28 100644 --- a/qualibrate/storage/local_storage_manager.py +++ b/qualibrate/storage/local_storage_manager.py @@ -189,10 +189,9 @@ def _save_old_quam(self, machine: MachineProtocol) -> None: for elem_group in proposed_content_mapping.values() for elem in elem_group ) - if all_attrs_present: - content_mapping = proposed_content_mapping - else: - content_mapping = None + content_mapping = ( + proposed_content_mapping if all_attrs_present else None + ) # Save as single file in data folder machine.save(Path(self.data_handler.path) / "quam_state.json") From f2b7eecf1a0205018d6f110f160b4ef60a1fbcf2 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Thu, 11 Dec 2025 13:41:59 +0200 Subject: [PATCH 17/73] linted code --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3860e29..82c7797 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,8 +58,8 @@ select = [ #"*.py" = ["E501"] # E501 will be ignored for exit code, but still displayed [tool.ruff.lint.pycodestyle] -max-line-length = 100 -max-doc-length = 100 +max-line-length = 120 +max-doc-length = 120 [tool.mypy] python_version = "3.10" From afc536082677c18db3622d651f864b5e6c5bae8c Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Thu, 11 Dec 2025 13:47:06 +0200 Subject: [PATCH 18/73] removed unused code --- pyproject.toml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 82c7797..0f5e882 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,11 +51,6 @@ select = [ "SIM", # flake8-simplify "I", # isort ] -#ignore = [ -# "E501" # ignore line-too-long errors -##] -#[tool.ruff.lint.per-file-ignores] -#"*.py" = ["E501"] # E501 will be ignored for exit code, but still displayed [tool.ruff.lint.pycodestyle] max-line-length = 120 From 57f0c52c54998e66afaf01a1d241e87697c501bc Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Wed, 17 Dec 2025 11:03:52 +0200 Subject: [PATCH 19/73] finished operational condition + connection failed conditional + fixed a bug when overiding a connection we raise an exception + fixed a bug that duplicated node cannot be in the execution queue + added unit testing and integration testing to operational condition and connection on failure optional(add a graph using it and run it aswell) + moved fixtures from test_graph to outer scope so it can be used in test_orchestrator --- qualibrate/models/operational_condition.py | 7 +- .../orchestration/basic_orchestrator.py | 23 +- qualibrate/qualibration_graph.py | 70 ++- .../graph_with_loop_condition_lambda.py | 1 + .../test_basic_orchestrator.py | 517 +++++++++++++++++- tests/unit/conftest.py | 88 +++ .../test_basic_orchestrator.py | 491 ++++++++++++++++- .../test_qualibration_graph/test_graph.py | 116 +--- .../test_graph_export_mixin.py | 5 +- .../test_graph_serialize.py | 14 +- 10 files changed, 1192 insertions(+), 140 deletions(-) create mode 100644 tests/unit/conftest.py diff --git a/qualibrate/models/operational_condition.py b/qualibrate/models/operational_condition.py index 280c3c8..6451c68 100644 --- a/qualibrate/models/operational_condition.py +++ b/qualibrate/models/operational_condition.py @@ -8,8 +8,6 @@ class OperationalCondition(BaseModel, Generic[GraphElementTypeVar]): - # on_failure: bool = False - on_scenario : Outcome on_function: Callable[[GraphElementTypeVar, TargetType], bool] | None = None on_generator: ( Callable[ @@ -20,3 +18,8 @@ class OperationalCondition(BaseModel, Generic[GraphElementTypeVar]): ] | None ) = None + + +class LoopCondition(OperationalCondition[GraphElementTypeVar]): + max_iterations: int | None = None + on_failure : bool = False diff --git a/qualibrate/orchestration/basic_orchestrator.py b/qualibrate/orchestration/basic_orchestrator.py index a04354d..5498cb8 100644 --- a/qualibrate/orchestration/basic_orchestrator.py +++ b/qualibrate/orchestration/basic_orchestrator.py @@ -124,6 +124,10 @@ def get_next_element(self) -> GraphElementTypeVar | None: """ while not self._execution_queue.empty(): element_to_run = self._execution_queue.get() + # Skip if already finished (handles duplicate queue entries) + if self.check_node_finished(element_to_run): + continue + if all( map( self.check_node_finished, self.nx_graph.pred[element_to_run] @@ -212,7 +216,7 @@ def _set_out_targets_for_element( # self.nx_graph.edges[element, successor]["operational_condition"] is of type OperationalCondition has_on_failed_successors = any( - self.nx_graph.edges[element, successor]["operational_condition"].on_scenario + self.nx_graph.edges[element, successor][QualibrationGraph.RUN_SCENARIO_FIELD] == Outcome.FAILED for successor in self.nx_graph.successors(element) ) @@ -225,9 +229,9 @@ def _set_out_targets_for_element( QualibrationGraph.EDGE_TARGETS_FIELD ] = ( successful_out_targets - if self.nx_graph.edges[element, successor]["operational_condition"].on_scenario + if self.nx_graph.edges[element, successor][QualibrationGraph.RUN_SCENARIO_FIELD] == Outcome.SUCCESSFUL - else self._execute_condition(self.nx_graph.edges[element, successor]["operational_condition"], + else self._execute_condition(self.nx_graph.edges[element, successor][QualibrationGraph.OPERATIONAL_CONDITION_FIELD], element, failed_out_targets) ) else: @@ -242,13 +246,16 @@ def _set_out_targets_for_element( QualibrationGraph.EDGE_TARGETS_FIELD ] = successful_out_targets - def _execute_condition(self,conditional_operation: OperationalCondition[GraphElementTypeVar], element: GraphElementTypeVar, targets: list[TargetType]) ->list[TargetType]: - if conditional_operation.on_generator is not None: - executed_condition = conditional_operation.on_generator() + def _execute_condition(self, operational_condition: OperationalCondition[GraphElementTypeVar], element: GraphElementTypeVar, targets: list[TargetType]) ->list[TargetType]: + if operational_condition.on_generator is not None: + executed_condition = operational_condition.on_generator() #priming the generator, we need to get to the point where the generator expects out two variables executed_condition.send(None) return [target for target in targets if executed_condition.send((element, target))] - return [target for target in targets if conditional_operation.on_function(element, target)] + elif operational_condition.on_function is not None: + return [target for target in targets if operational_condition.on_function(element, target)] + # No condition specified, return all targets + return targets @@ -296,7 +303,7 @@ def _is_loop_iteration_needed( yield False return if ( - conditions.on_scenario == Outcome.FAILED + conditions.on_failure and nx_graph.nodes[element_to_run][ QualibrationGraph.ELEMENT_STATUS_FIELD ] diff --git a/qualibrate/qualibration_graph.py b/qualibrate/qualibration_graph.py index 1a6e912..6129b4f 100644 --- a/qualibrate/qualibration_graph.py +++ b/qualibrate/qualibration_graph.py @@ -1,10 +1,11 @@ import copy +import inspect import traceback from collections import defaultdict from collections.abc import Callable, Generator, Mapping, Sequence from contextvars import Token from datetime import datetime -from inspect import isgeneratorfunction +from inspect import isgeneratorfunction, Signature from pathlib import Path from types import TracebackType from typing import ( @@ -16,12 +17,12 @@ ) import networkx as nx -from pydantic import BaseModel, create_model +from pydantic import create_model from typing_extensions import Self from qualibrate.models.node_status import ElementRunStatus from qualibrate.models.outcome import Outcome -from qualibrate.models.operational_condition import OperationalCondition +from qualibrate.models.operational_condition import LoopCondition, OperationalCondition from qualibrate.models.run_mode import RunModes from qualibrate.models.run_summary.base import BaseRunSummary from qualibrate.models.run_summary.graph import GraphRunSummary @@ -87,8 +88,6 @@ # """ # Common amount for all loop types (on failure, on function, on generator) # """ -class LoopCondition(OperationalCondition[GraphElementTypeVar]): - max_iterations: int | None = None class QualibrationGraph( QRunnable[GraphCreateParametersType, GraphRunParametersType], @@ -125,6 +124,8 @@ class QualibrationGraph( EDGE_TARGETS_FIELD = "targets" ELEMENT_STATUS_FIELD = "status" LOOP_TARGETS_FIELD = "loop_targets" + OPERATIONAL_CONDITION_FIELD = "operational_condition" + RUN_SCENARIO_FIELD = "scenario" _node_init_args = { ELEMENT_STATUS_FIELD: ElementRunStatus.pending, } @@ -146,7 +147,7 @@ def __init__( if not isinstance(parameters, GraphParameters): raise ValueError("Graph parameters must be of type GraphParameters") super().__init__(name, parameters, description=description, modes=modes) - self._connectivity: dict[tuple[str, str], OperationalCondition[GraphElementTypeVar]] + self._connectivity: dict[tuple[str, str], dict[str,Any]] if finalize: if nodes is None or connectivity is None: raise RuntimeError( @@ -156,7 +157,8 @@ def __init__( self._elements = dict(nodes) self._connectivity = { # connectivity: Outcome.SUCCESSFUL - connectivity: OperationalCondition[GraphElementTypeVar](on_scenario=Outcome.SUCCESSFUL) + connectivity: {QualibrationGraph.RUN_SCENARIO_FIELD: Outcome.SUCCESSFUL, + QualibrationGraph.OPERATIONAL_CONDITION_FIELD:OperationalCondition[GraphElementTypeVar]()} for connectivity in connectivity } else: @@ -268,7 +270,8 @@ def __copy__(self) -> Self: new_graph._graph.add_edge( new_graph._elements[source], new_graph._elements[destination], - operational_condition=copy.deepcopy(self._connectivity[(source, destination)]), + scenario = self._connectivity[(source,destination)][QualibrationGraph.RUN_SCENARIO_FIELD], + operational_condition=copy.deepcopy(self._connectivity[(source, destination)][QualibrationGraph.OPERATIONAL_CONDITION_FIELD]), ) # Copy orchestrator if it exists @@ -350,7 +353,8 @@ def _add_nodes_and_connections_to_nx(self) -> None: self._graph.add_edge( source_element, destination_element, - operational_condition=self._connectivity[(source, destination)], + scenario = self._connectivity[(source,destination)][QualibrationGraph.RUN_SCENARIO_FIELD], + operational_condition=self._connectivity[(source, destination)][QualibrationGraph.OPERATIONAL_CONDITION_FIELD], ) self._validate_graph_acyclic() @@ -864,7 +868,7 @@ def __serialize_graph_represantation_inner( node_conditions.on_generator is not None or node_conditions.on_function is not None ) - loop_data["on_failure"] = node_conditions.on_scenario == Outcome.FAILED + loop_data["on_failure"] = node_conditions.on_failure loop_data["max_iterations"] = node_conditions.max_iterations if isinstance(element, QualibrationGraph): @@ -886,7 +890,15 @@ def __serialize_graph_represantation_inner( for adj in adjacency: target_name = adj["id"] - + #where QualibrationGraph.OPERATIONAL_CONDITION_FIELD is used, it's because it is how we save the operational condition in the graph + #where it's not used but operational_condition key is still use, it's because this is the key returned to the ui and not necessarily the key we save the data in + #same as for scenario + condition_name, condition_content = self._get_operational_condition_signature_and_content(adj[QualibrationGraph.OPERATIONAL_CONDITION_FIELD]) + operational_condition_data = {'operational_condition': False} + if condition_name and condition_content: + operational_condition_data["operational_condition"] = True + operational_condition_data["condition_label"] = condition_name + operational_condition_data["condition_description"] = condition_content flow_dict["edges"].append( { "id": f"{node_id}->" @@ -894,10 +906,11 @@ def __serialize_graph_represantation_inner( "source": node_id, "target": name_identifier_dict[target_name], "data": { - "condition": adj.get( - "scenario", Outcome.SUCCESSFUL + "connect": adj.get( + QualibrationGraph.RUN_SCENARIO_FIELD, Outcome.SUCCESSFUL ) == Outcome.SUCCESSFUL, + **operational_condition_data, }, } ) @@ -1091,7 +1104,7 @@ def connect( src: str | GraphElementTypeVar, dst: str | GraphElementTypeVar, ) -> None: - self._connect(src, dst) + self._connect(src=src, dst=dst,run_scenario=Outcome.SUCCESSFUL) @ensure_not_finalized @ensure_building @@ -1100,7 +1113,6 @@ def connect_on_failure( /, src: str | GraphElementTypeVar, dst: str | GraphElementTypeVar, - element: str | GraphElementTypeVar, on: ( Callable[ [], @@ -1122,8 +1134,8 @@ def connect_on_failure( on_function = cast( Callable[[GraphElementTypeVar, TargetType], bool], on ) - operational_condition = OperationalCondition(on_function = on_function, on_generator = on_generator, on_scenario=Outcome.FAILED) - self._connect(src, dst, operational_condition) + operational_condition = OperationalCondition(on_function = on_function, on_generator = on_generator) + self._connect(src=src, dst=dst,run_scenario=Outcome.FAILED,operational_condition= operational_condition) @ensure_not_finalized @ensure_building @@ -1131,7 +1143,7 @@ def _connect( self, src: str | GraphElementTypeVar, dst: str | GraphElementTypeVar, - # run_scenario: Outcome, + run_scenario: Outcome, operational_condition: OperationalCondition[GraphElementTypeVar] = None, ) -> None: s = self._resolve_element_name(src) @@ -1141,10 +1153,13 @@ def _connect( f"Both '{s}' and '{d}' must be added before connecting." ) edge = (s, d) - self._connectivity[edge] = operational_condition if operational_condition else OperationalCondition(on_scenario=Outcome.SUCCESSFUL) + #bug fix it + if edge in self._connectivity: + raise ValueError(f"Edge '{edge}' already exists.") + self._connectivity[edge] ={QualibrationGraph.RUN_SCENARIO_FIELD : run_scenario, + QualibrationGraph.OPERATIONAL_CONDITION_FIELD: operational_condition if operational_condition else OperationalCondition()} + - # if edge in self._connectivity: - # return # self._connectivity[edge] = run_scenario def _get_validated_element_name( @@ -1196,4 +1211,15 @@ def _loop_on_failure( element_name = self._get_validated_element_name(element) conditions = self._loop_conditions[element_name] conditions.max_iterations = max_iterations - conditions.on_scenario = Outcome.FAILED + conditions.on_failure = True + + def _get_operational_condition_signature_and_content(self,operational_condition) -> tuple[str, str] | tuple[None, None]: + func = None + if operational_condition.on_function is not None: + func = operational_condition.on_function + elif operational_condition.on_generator is not None: + func = operational_condition.on_generator + if func is not None: + return func.__name__, inspect.getsource(func) + else: + return None,None \ No newline at end of file diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py index a5ce72e..72f7108 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py @@ -13,6 +13,7 @@ on=lambda node, target: node.results[target]["fidelity"] < 0.95, ) graph.connect("node", "node2") + # graph.connect_on_failure("node","node2", on=lambda x,y:True) if __name__ == "__main__": result = graph.run() diff --git a/tests/integration/test_orchestration/test_basic_orchestrator.py b/tests/integration/test_orchestration/test_basic_orchestrator.py index 9c90173..282d402 100644 --- a/tests/integration/test_orchestration/test_basic_orchestrator.py +++ b/tests/integration/test_orchestration/test_basic_orchestrator.py @@ -15,10 +15,14 @@ from qualibrate.orchestration.basic_orchestrator import BasicOrchestrator from qualibrate.parameters import ( GraphParameters, + RunnableParameters ) from qualibrate.qualibration_graph import QualibrationGraph from qualibrate.qualibration_library import QualibrationLibrary - +from unittest.mock import MagicMock +from qualibrate import QualibrationNode +from qualibrate.models.operational_condition import OperationalCondition +from qualibrate.models.run_summary.node import NodeRunSummary @pytest.fixture def qualibration_lib( @@ -165,3 +169,514 @@ def test_run_sequence_with_error( }, ), ) + +def test_traverse_graph_with_conditional_failed_edge_filters_targets(): + """ + Integration test: Verify that operational conditions on failed edges + filter targets correctly during graph traversal. + + Graph structure: + node1 (root) - some targets fail with retriable errors + ├─[SUCCESS]──────────> node2_success + └─[FAILED w/ condition]> node3_retry (only retries retriable errors) + + Expected: + - node1 runs with all targets (set by orchestrator) + - Successful targets go to node2_success (set by orchestrator) + - Failed targets are filtered by condition before going to node3_retry (set by orchestrator) + """ + orchestrator = BasicOrchestrator(skip_failed=False) + + # Create mock nodes + node1 = MagicMock(spec=QualibrationNode) + node1.name = "node1" + node1.parameters = MagicMock(spec=RunnableParameters) + node1.outcomes = {} + node1.description = None + + node2_success = MagicMock(spec=QualibrationNode) + node2_success.name = "node2_success" + node2_success.parameters = MagicMock(spec=RunnableParameters) + node2_success.outcomes = {} + node2_success.description = None + + node3_retry = MagicMock(spec=QualibrationNode) + node3_retry.name = "node3_retry" + node3_retry.parameters = MagicMock(spec=RunnableParameters) + node3_retry.outcomes = {} + node3_retry.description = None + + # node1 will track error types for filtering + node1.results = { + "q1": {"status": "success"}, + "q2": {"status": "failed", "error_type": "permanent"}, + "q3": {"status": "failed", "error_type": "retriable"}, + "q4": {"status": "failed", "error_type": "retriable"}, + } + + # Create mock graph + mock_graph = MagicMock(spec=QualibrationGraph) + mock_graph.name = "test_graph_with_conditional_retry" + mock_graph._loop_conditions = {} + + # Setup parameters + mock_params = MagicMock() + node1_params = MagicMock() + node1_params.targets = None + node1_params.model_dump.return_value = {} + node2_params = MagicMock() + node2_params.targets = None + node2_params.model_dump.return_value = {} + node3_params = MagicMock() + node3_params.targets = None + node3_params.model_dump.return_value = {} + + mock_params.node1 = node1_params + mock_params.node2_success = node2_params + mock_params.node3_retry = node3_params + mock_graph.full_parameters.nodes = mock_params + + # Create NetworkX graph + import networkx as nx + + nx_graph = nx.DiGraph() + + nx_graph.add_node(node1, status=ElementRunStatus.pending, retries=0) + nx_graph.add_node(node2_success, status=ElementRunStatus.pending, retries=0) + nx_graph.add_node(node3_retry, status=ElementRunStatus.pending, retries=0) + + # Operational condition: only retry if error_type is "retriable" + retry_condition = OperationalCondition( + on_function=lambda node, target: node.results[target]["error_type"] == "retriable" + ) + + # Add edges + nx_graph.add_edge( + node1, + node2_success, + scenario=Outcome.SUCCESSFUL, + operational_condition=OperationalCondition() + ) + nx_graph.add_edge( + node1, + node3_retry, + scenario=Outcome.FAILED, + operational_condition=retry_condition + ) + + mock_graph._graph = nx_graph + + # Track execution + execution_log = [] + + def node1_run(interactive=False, **kwargs): + targets_received = node1_params.targets[:] + execution_log.append(("node1", targets_received)) + + # Create run summary + summary = MagicMock(spec=NodeRunSummary) + summary.successful_targets = ["q1"] + summary.failed_targets = ["q2", "q3", "q4"] + summary.initial_targets = targets_received + + # Set run_summary on the node (orchestrator will read this) + node1.run_summary = summary + return summary + + def node2_run(interactive=False, **kwargs): + targets_received = node2_params.targets[:] + execution_log.append(("node2_success", targets_received)) + + # Create run summary + summary = MagicMock(spec=NodeRunSummary) + summary.successful_targets = targets_received + summary.failed_targets = [] + summary.initial_targets = targets_received + + # Set run_summary on the node + node2_success.run_summary = summary + return summary + + def node3_run(interactive=False, **kwargs): + targets_received = node3_params.targets[:] + execution_log.append(("node3_retry", targets_received)) + + # Create run summary + summary = MagicMock(spec=NodeRunSummary) + summary.successful_targets = targets_received + summary.failed_targets = [] + summary.initial_targets = targets_received + + # Set run_summary on the node + node3_retry.run_summary = summary + return summary + + node1.run = node1_run + node2_success.run = node2_run + node3_retry.run = node3_run + + # Run traversal + orchestrator.traverse_graph(mock_graph, ["q1", "q2", "q3", "q4"]) + + # Verify execution + assert len(execution_log) == 3 + + # node1 receives all targets + assert execution_log[0][0] == "node1" + assert set(execution_log[0][1]) == {"q1", "q2", "q3", "q4"} + + # node2_success receives only successful targets + assert execution_log[1][0] == "node2_success" + assert set(execution_log[1][1]) == {"q1"} + + # node3_retry receives only retriable failed targets (q3, q4) + # NOT q2 (permanent error) - filtered by operational condition + assert execution_log[2][0] == "node3_retry" + assert set(execution_log[2][1]) == {"q3", "q4"} + + # Verify all nodes finished + assert nx_graph.nodes[node1][QualibrationGraph.ELEMENT_STATUS_FIELD] == ElementRunStatus.finished + assert nx_graph.nodes[node2_success][QualibrationGraph.ELEMENT_STATUS_FIELD] == ElementRunStatus.finished + assert nx_graph.nodes[node3_retry][QualibrationGraph.ELEMENT_STATUS_FIELD] == ElementRunStatus.finished + + +def test_traverse_graph_with_generator_condition_on_failed_edge(): + """ + Integration test: Verify generator-based operational conditions work + during graph traversal. + + Graph structure: + node1 (root) - targets fail with different retry counts + ├─[SUCCESS]──────────────> node2_success + └─[FAILED w/ generator]──> node3_retry (only if retries < max) + """ + orchestrator = BasicOrchestrator(skip_failed=False) + + # Create mock nodes + node1 = MagicMock(spec=QualibrationNode) + node1.name = "node1" + node1.max_retries = 3 + node1.parameters = MagicMock(spec=RunnableParameters) + node1.outcomes = {} + node1.description = None + + node2_success = MagicMock(spec=QualibrationNode) + node2_success.name = "node2_success" + node2_success.parameters = MagicMock(spec=RunnableParameters) + node2_success.outcomes = {} + node2_success.description = None + + node3_retry = MagicMock(spec=QualibrationNode) + node3_retry.name = "node3_retry" + node3_retry.parameters = MagicMock(spec=RunnableParameters) + node3_retry.outcomes = {} + node3_retry.description = None + + # node1: mixed results with retry counts + node1.results = { + "q1": {"status": "success", "retries": 0}, + "q2": {"status": "failed", "retries": 2}, # Can retry + "q3": {"status": "failed", "retries": 3}, # Max retries reached + "q4": {"status": "failed", "retries": 1}, # Can retry + } + + # Create mock graph + mock_graph = MagicMock(spec=QualibrationGraph) + mock_graph.name = "test_graph_with_generator_condition" + mock_graph._loop_conditions = {} + + # Setup parameters + mock_params = MagicMock() + node1_params = MagicMock() + node1_params.targets = None + node1_params.model_dump.return_value = {} + node2_params = MagicMock() + node2_params.targets = None + node2_params.model_dump.return_value = {} + node3_params = MagicMock() + node3_params.targets = None + node3_params.model_dump.return_value = {} + + mock_params.node1 = node1_params + mock_params.node2_success = node2_params + mock_params.node3_retry = node3_params + mock_graph.full_parameters.nodes = mock_params + + # Create NetworkX graph + import networkx as nx + + nx_graph = nx.DiGraph() + + nx_graph.add_node(node1, status=ElementRunStatus.pending, retries=0) + nx_graph.add_node(node2_success, status=ElementRunStatus.pending, retries=0) + nx_graph.add_node(node3_retry, status=ElementRunStatus.pending, retries=0) + + # Generator condition: only retry if retries < max_retries + def retry_if_not_exhausted(): + while True: + element, target = yield + yield element.results[target]["retries"] < element.max_retries + + retry_condition = OperationalCondition(on_generator=retry_if_not_exhausted) + + # Add edges + nx_graph.add_edge( + node1, + node2_success, + scenario=Outcome.SUCCESSFUL, + operational_condition=OperationalCondition() + ) + nx_graph.add_edge( + node1, + node3_retry, + scenario=Outcome.FAILED, + operational_condition=retry_condition + ) + + mock_graph._graph = nx_graph + + # Track execution + execution_log = [] + + def node1_run(interactive=False, **kwargs): + targets_received = node1_params.targets[:] + execution_log.append(("node1", targets_received)) + + # Create run summary + summary = MagicMock(spec=NodeRunSummary) + summary.successful_targets = ["q1"] + summary.failed_targets = ["q2", "q3", "q4"] + summary.initial_targets = targets_received + + # Set run_summary on the node + node1.run_summary = summary + return summary + + def node2_run(interactive=False, **kwargs): + targets_received = node2_params.targets[:] + execution_log.append(("node2_success", targets_received)) + + # Create run summary + summary = MagicMock(spec=NodeRunSummary) + summary.successful_targets = targets_received + summary.failed_targets = [] + summary.initial_targets = targets_received + + # Set run_summary on the node + node2_success.run_summary = summary + return summary + + def node3_run(interactive=False, **kwargs): + targets_received = node3_params.targets[:] + execution_log.append(("node3_retry", targets_received)) + + # Create run summary + summary = MagicMock(spec=NodeRunSummary) + summary.successful_targets = targets_received + summary.failed_targets = [] + summary.initial_targets = targets_received + + # Set run_summary on the node + node3_retry.run_summary = summary + return summary + + node1.run = node1_run + node2_success.run = node2_run + node3_retry.run = node3_run + + # Run traversal + orchestrator.traverse_graph(mock_graph, ["q1", "q2", "q3", "q4"]) + + # Verify execution + assert len(execution_log) == 3 + + # node1 receives all targets + assert execution_log[0][0] == "node1" + assert set(execution_log[0][1]) == {"q1", "q2", "q3", "q4"} + + # node2_success receives successful targets + assert execution_log[1][0] == "node2_success" + assert set(execution_log[1][1]) == {"q1"} + + # node3_retry receives only targets with retries < max (q2, q4) + # NOT q3 (retries=3, max=3) + assert execution_log[2][0] == "node3_retry" + assert set(execution_log[2][1]) == {"q2", "q4"} + + # Verify all nodes finished + assert nx_graph.nodes[node1][QualibrationGraph.ELEMENT_STATUS_FIELD] == ElementRunStatus.finished + assert nx_graph.nodes[node2_success][QualibrationGraph.ELEMENT_STATUS_FIELD] == ElementRunStatus.finished + assert nx_graph.nodes[node3_retry][QualibrationGraph.ELEMENT_STATUS_FIELD] == ElementRunStatus.finished + + +def test_traverse_graph_multiple_failed_edges_different_conditions(): + """ + Integration test: Multiple failed edges with different conditions route + targets to different handlers. + + Graph structure: + node1 (root) - targets fail with different error types + ├─[SUCCESS]─────────────────> node2_success + ├─[FAILED w/ timeout cond]──> node3_timeout_handler + └─[FAILED w/ hw cond]───────> node4_hw_handler + """ + from qualibrate.models.operational_condition import OperationalCondition + + orchestrator = BasicOrchestrator(skip_failed=False) + + # Create mock nodes + node1 = MagicMock(spec=QualibrationNode) + node1.name = "node1" + node1.snapshot_idx = None + node1.parameters = MagicMock(spec=RunnableParameters) + node1.outcomes = {} + node1.description = None + + node2_success = MagicMock(spec=QualibrationNode) + node2_success.name = "node2_success" + node2_success.snapshot_idx = None + node2_success.parameters = MagicMock(spec=RunnableParameters) + node2_success.outcomes = {} + node2_success.description = None + + node3_timeout = MagicMock(spec=QualibrationNode) + node3_timeout.name = "node3_timeout_handler" + node3_timeout.snapshot_idx = None + node3_timeout.parameters = MagicMock(spec=RunnableParameters) + node3_timeout.outcomes = {} + node3_timeout.description = None + + node4_hw = MagicMock(spec=QualibrationNode) + node4_hw.name = "node4_hw_handler" + node4_hw.snapshot_idx = None + node4_hw.parameters = MagicMock(spec=RunnableParameters) + node4_hw.outcomes = {} + node4_hw.description = None + + # node1: different error types + node1.results = { + "q1": {"status": "success"}, + "q2": {"status": "failed", "error": "timeout"}, + "q3": {"status": "failed", "error": "hardware"}, + "q4": {"status": "failed", "error": "timeout"}, + "q5": {"status": "failed", "error": "hardware"}, + } + + # Create mock graph + mock_graph = MagicMock(spec=QualibrationGraph) + mock_graph.name = "test_graph_multiple_error_handlers" + mock_graph._loop_conditions = {} + + # Setup parameters + mock_params = MagicMock() + for name in ["node1", "node2_success", "node3_timeout_handler", "node4_hw_handler"]: + params = MagicMock() + params.targets = None + params.model_dump.return_value = {} + setattr(mock_params, name, params) + + mock_graph.full_parameters.nodes = mock_params + + # Create NetworkX graph + import networkx as nx + + nx_graph = nx.DiGraph() + + for node in [node1, node2_success, node3_timeout, node4_hw]: + nx_graph.add_node(node, status=ElementRunStatus.pending, retries=0) + + # Conditions for different error types + timeout_condition = OperationalCondition( + on_function=lambda n, t: n.results[t]["error"] == "timeout" + ) + hw_condition = OperationalCondition( + on_function=lambda n, t: n.results[t]["error"] == "hardware" + ) + + # Add edges + nx_graph.add_edge(node1, node2_success, scenario=Outcome.SUCCESSFUL, operational_condition=OperationalCondition()) + nx_graph.add_edge(node1, node3_timeout, scenario=Outcome.FAILED, operational_condition=timeout_condition) + nx_graph.add_edge(node1, node4_hw, scenario=Outcome.FAILED, operational_condition=hw_condition) + + mock_graph._graph = nx_graph + + # Track execution + execution_log = {} + + def node1_run(interactive=False, **kwargs): + params = getattr(mock_params, "node1") + targets_received = params.targets[:] + execution_log["node1"] = targets_received + + # Create run summary + summary = MagicMock(spec=NodeRunSummary) + summary.successful_targets = ["q1"] + summary.failed_targets = ["q2", "q3", "q4", "q5"] + summary.initial_targets = targets_received + + # Set run_summary on the node + node1.run_summary = summary + return summary + + def node2_run(interactive=False, **kwargs): + params = getattr(mock_params, "node2_success") + targets_received = params.targets[:] + execution_log["node2_success"] = targets_received + + # Create run summary + summary = MagicMock(spec=NodeRunSummary) + summary.successful_targets = targets_received + summary.failed_targets = [] + summary.initial_targets = targets_received + + # Set run_summary on the node + node2_success.run_summary = summary + return summary + + def node3_run(interactive=False, **kwargs): + params = getattr(mock_params, "node3_timeout_handler") + targets_received = params.targets[:] + execution_log["node3_timeout_handler"] = targets_received + + # Create run summary + summary = MagicMock(spec=NodeRunSummary) + summary.successful_targets = targets_received + summary.failed_targets = [] + summary.initial_targets = targets_received + + # Set run_summary on the node + node3_timeout.run_summary = summary + return summary + + def node4_run(interactive=False, **kwargs): + params = getattr(mock_params, "node4_hw_handler") + targets_received = params.targets[:] + execution_log["node4_hw_handler"] = targets_received + + # Create run summary + summary = MagicMock(spec=NodeRunSummary) + summary.successful_targets = targets_received + summary.failed_targets = [] + summary.initial_targets = targets_received + + # Set run_summary on the node + node4_hw.run_summary = summary + return summary + + node1.run = node1_run + node2_success.run = node2_run + node3_timeout.run = node3_run + node4_hw.run = node4_run + + # Run traversal + orchestrator.traverse_graph(mock_graph, ["q1", "q2", "q3", "q4", "q5"]) + + # Verify routing + assert set(execution_log["node1"]) == {"q1", "q2", "q3", "q4", "q5"} + assert set(execution_log["node2_success"]) == {"q1"} + assert set(execution_log["node3_timeout_handler"]) == {"q2", "q4"} + assert set(execution_log["node4_hw_handler"]) == {"q3", "q5"} + + # Verify all nodes finished + for node in [node1, node2_success, node3_timeout, node4_hw]: + assert nx_graph.nodes[node][QualibrationGraph.ELEMENT_STATUS_FIELD] == ElementRunStatus.finished diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 0000000..adef161 --- /dev/null +++ b/tests/unit/conftest.py @@ -0,0 +1,88 @@ +from unittest.mock import MagicMock + +import pytest + +from qualibrate import QualibrationNode, QualibrationGraph +from qualibrate.q_runnnable import QRunnable + + +@pytest.fixture +def mock_logger( mocker): + return mocker.patch("qualibrate.qualibration_graph.logger") + + +@pytest.fixture +def mock_library( mocker): + """Mock the QualibrationLibrary to avoid initialization issues""" + mock_lib = mocker.MagicMock() + mock_lib.nodes.values_nocopy.return_value = {} # Empty library + mocker.patch.object( + QualibrationGraph, + "_get_library", + return_value=mock_lib, + ) + return mock_lib + + +@pytest.fixture +def mock_orchestrator( mocker): + return mocker.patch( + "qualibrate.orchestration.basic_orchestrator.QualibrationOrchestrator" + ) + + +@pytest.fixture +def pre_setup_graph_nodes(): + node1 = MagicMock(QualibrationNode) + node1.name = "node1" + node2 = MagicMock(QualibrationNode) + node2.name = "node2" + return {"node1": node1, "node2": node2} + + +@pytest.fixture +def mocked__validate_no_elements_from_library( mocker): + return mocker.patch( + "qualibrate.qualibration_graph.QualibrationGraph" + "._validate_no_elements_from_library" + ) + + +@pytest.fixture +def pre_setup_graph_parameters_build( + mocker, mocked__validate_no_elements_from_library +): + mocked_build_base_parameters = mocker.patch.object( + QRunnable, "build_parameters_class_from_instance" + ) + mocked_build_full_parameters = mocker.patch( + "qualibrate.qualibration_graph.QualibrationGraph" + "._build_parameters_class" + ) + return ( + mocked_build_base_parameters, + mocked_build_full_parameters, + mocked__validate_no_elements_from_library, + ) + + +@pytest.fixture +def pre_setup_graph_init( + mocker, pre_setup_graph_nodes, pre_setup_graph_parameters_build +): + mocked_add_nodes_and_connections = mocker.patch( + "qualibrate.qualibration_graph.QualibrationGraph." + "_add_nodes_and_connections_to_nx", + ) + ( + mocked_build_base_parameters, + mocked_build_full_parameters, + mocked__validate_no_elements_from_library, + ) = pre_setup_graph_parameters_build + + return ( + pre_setup_graph_nodes, + mocked_add_nodes_and_connections, + mocked_build_base_parameters, + mocked_build_full_parameters, + ) \ No newline at end of file diff --git a/tests/unit/test_orchestration/test_basic_orchestrator.py b/tests/unit/test_orchestration/test_basic_orchestrator.py index 8f38e9b..0de8acf 100644 --- a/tests/unit/test_orchestration/test_basic_orchestrator.py +++ b/tests/unit/test_orchestration/test_basic_orchestrator.py @@ -5,10 +5,11 @@ from qualibrate import QualibrationNode from qualibrate.models.node_status import ElementRunStatus +from qualibrate.models.operational_condition import OperationalCondition from qualibrate.models.outcome import Outcome from qualibrate.models.run_summary.node import NodeRunSummary from qualibrate.orchestration.basic_orchestrator import BasicOrchestrator -from qualibrate.parameters import RunnableParameters +from qualibrate.parameters import RunnableParameters, GraphParameters from qualibrate.qualibration_graph import QualibrationGraph @@ -138,7 +139,7 @@ def test_get_next_node_returns_node(self, mocker): # Mock check_node_finished to always return True mocker.patch.object( - orchestrator, "check_node_finished", return_value=True + orchestrator, "check_node_finished", side_effect=[False,True] ) assert orchestrator.get_next_element() == mock_node @@ -341,8 +342,14 @@ def test_set_out_targets_with_failed_successor(self, mocker): mock_edges = MagicMock() # Create actual dict objects that can be modified - success_edge_data = {"scenario": Outcome.SUCCESSFUL} - failure_edge_data = {"scenario": Outcome.FAILED} + success_edge_data = { + "scenario": Outcome.SUCCESSFUL, + "operational_condition": OperationalCondition() + } + failure_edge_data = { + "scenario": Outcome.FAILED, + "operational_condition": OperationalCondition() # Empty condition + } def edges_getitem(key): element, successor = key @@ -536,8 +543,8 @@ def test_traverse_graph_with_success_and_failure_paths(self, mocker): ) # Add edges with scenarios - nx_graph.add_edge(node1, node2_success, scenario=Outcome.SUCCESSFUL) - nx_graph.add_edge(node1, node3_failure, scenario=Outcome.FAILED) + nx_graph.add_edge(node1, node2_success, scenario=Outcome.SUCCESSFUL,operational_condition=OperationalCondition()) + nx_graph.add_edge(node1, node3_failure, scenario=Outcome.FAILED,operational_condition=OperationalCondition()) mock_graph._graph = nx_graph @@ -675,8 +682,8 @@ def test_traverse_graph_all_targets_succeed_no_failure_path(self, mocker): node3_failure, status=ElementRunStatus.pending, retries=0 ) - nx_graph.add_edge(node1, node2_success, scenario=Outcome.SUCCESSFUL) - nx_graph.add_edge(node1, node3_failure, scenario=Outcome.FAILED) + nx_graph.add_edge(node1, node2_success, scenario=Outcome.SUCCESSFUL, operational_condition=OperationalCondition()) + nx_graph.add_edge(node1, node3_failure, scenario=Outcome.FAILED, operational_condition=OperationalCondition()) mock_graph._graph = nx_graph @@ -801,8 +808,8 @@ def test_traverse_graph_success_path_with_no_targets_is_skipped(self): node3_failure, status=ElementRunStatus.pending, retries=0 ) - nx_graph.add_edge(node1, node2_success, scenario=Outcome.SUCCESSFUL) - nx_graph.add_edge(node1, node3_failure, scenario=Outcome.FAILED) + nx_graph.add_edge(node1, node2_success, scenario=Outcome.SUCCESSFUL, operational_condition=OperationalCondition()) + nx_graph.add_edge(node1, node3_failure, scenario=Outcome.FAILED, operational_condition=OperationalCondition()) mock_graph._graph = nx_graph @@ -840,3 +847,467 @@ def node3_run(interactive=False, **kwargs): ] == ElementRunStatus.finished ) + + +def test_execute_condition_with_lambda_function(): + """Test _execute_condition with a simple function condition""" + + orchestrator = BasicOrchestrator() + + mock_element = MagicMock() + mock_element.results = { + "q1": {"value": 10}, + "q2": {"value": 5}, + "q3": {"value": 15} + } + + # Condition: filter targets where value > 8 + condition = OperationalCondition( + on_function=lambda el, target: el.results[target]["value"] > 8 + ) + + targets = ["q1", "q2", "q3"] + result = orchestrator._execute_condition(condition, mock_element, targets) + + assert set(result) == {"q1", "q3"} + +def test_execute_condition_with_function(): + """Test _execute_condition with a simple function condition""" + + orchestrator = BasicOrchestrator() + + mock_element = MagicMock() + mock_element.results = { + "q1": {"value": 10}, + "q2": {"value": 5}, + "q3": {"value": 15} + } + def condition_function(el, target): + return el.results[target]["value"] > 8 + # Condition: filter targets where value > 8 + condition = OperationalCondition( + on_function=condition_function + ) + + targets = ["q1", "q2", "q3"] + result = orchestrator._execute_condition(condition, mock_element, targets) + + assert set(result) == {"q1", "q3"} + +def test_execute_condition_with_generator(): + """Test _execute_condition with a generator condition""" + from qualibrate.models.operational_condition import OperationalCondition + + orchestrator = BasicOrchestrator() + + mock_element = MagicMock() + mock_element.results = { + "q1": {"fidelity": 0.96}, + "q2": {"fidelity": 0.92}, + "q3": {"fidelity": 0.98} + } + + # Generator condition: filter targets where fidelity > 0.95 + def condition_gen(): + while True: + element, target = yield + yield element.results[target]["fidelity"] > 0.95 + + condition = OperationalCondition(on_generator=condition_gen) + + targets = ["q1", "q2", "q3"] + result = orchestrator._execute_condition(condition, mock_element, targets) + + assert set(result) == {"q1", "q3"} + + +def test_set_out_targets_with_operational_condition_on_failure( mocker): + """Test _set_out_targets routes targets through operational condition on failed edge""" + orchestrator = BasicOrchestrator() + + mock_node = MagicMock() + mock_node.results = { + "q1": {"error_count": 0}, + "q2": {"error_count": 5}, + "q3": {"error_count": 2}, + "q4": {"error_count": 5}, + } + + mock_success_successor = MagicMock() + mock_failure_successor = MagicMock() + + # Mock run summary with some failed targets + mock_summary = MagicMock(spec=NodeRunSummary) + mock_summary.successful_targets = ["q1", "q2"] + mock_summary.failed_targets = ["q4", "q3"] + mock_node.run_summary = mock_summary + + mock_nx_graph = mocker.patch( + "qualibrate.orchestration.basic_orchestrator.BasicOrchestrator.nx_graph", + new_callable=PropertyMock, + ) + mock_nx_graph.return_value.successors.return_value = [ + mock_success_successor, + mock_failure_successor, + ] + + + # Operational condition: only route targets with error_count <= 3 + op_condition = OperationalCondition( + on_function=lambda node, target: node.results[target]["error_count"] <= 3 + ) + + mock_edges = MagicMock() + + success_edge_data = {"scenario": Outcome.SUCCESSFUL} + failure_edge_data = { + "scenario": Outcome.FAILED, + "operational_condition": op_condition + } + + def edges_getitem(key): + element, successor = key + if successor == mock_success_successor: + return success_edge_data + elif successor == mock_failure_successor: + return failure_edge_data + raise KeyError(key) + + mock_edges.__getitem__.side_effect = edges_getitem + mock_nx_graph.return_value.edges = mock_edges + + orchestrator._graph = MagicMock() + + orchestrator._set_out_targets_for_element(mock_node) + + # Successful edge should get successful targets + assert success_edge_data[QualibrationGraph.EDGE_TARGETS_FIELD] == ["q1", "q2"] + + # Failed edge should get failed targets filtered by condition (q3 has error_count=2 <= 3) + assert failure_edge_data[QualibrationGraph.EDGE_TARGETS_FIELD] == ["q3"] + + +def test_connect_on_failure_with_condition_lambda_function( pre_setup_graph_init, mock_library): + """Test that connect_on_failure() with condition function works correctly""" + (nodes, _, _, _) = pre_setup_graph_init + + with QualibrationGraph.build( + name="test_graph", + parameters=GraphParameters(), + ) as graph: + graph.add_nodes(nodes["node1"], nodes["node2"]) + graph.connect_on_failure( + "node1", + "node2", + on=lambda element, target: True + ) + + # Check connectivity + assert ("node1", "node2") in graph._connectivity + assert graph._connectivity[("node1", "node2")][QualibrationGraph.RUN_SCENARIO_FIELD] == Outcome.FAILED + assert graph._connectivity[("node1", "node2")][ + QualibrationGraph.OPERATIONAL_CONDITION_FIELD].on_function is not None + +def test_connect_on_failure_with_condition_generator( pre_setup_graph_init, mock_library): + """Test that connect_on_failure() with generator condition works correctly""" + (nodes, _, _, _) = pre_setup_graph_init + + def condition_gen(): + while True: + element, target = yield + yield True + + with QualibrationGraph.build( + name="test_graph", + parameters=GraphParameters(), + ) as graph: + graph.add_nodes(nodes["node1"], nodes["node2"]) + graph.connect_on_failure("node1", "node2", on=condition_gen) + + # Check connectivity + assert ("node1", "node2") in graph._connectivity + assert graph._connectivity[("node1", "node2")][QualibrationGraph.RUN_SCENARIO_FIELD] == Outcome.FAILED + assert graph._connectivity[("node1", "node2")][ + QualibrationGraph.OPERATIONAL_CONDITION_FIELD].on_generator is not None + +def test_get_next_element_skips_already_finished_node( mocker): + """Test that get_next_element skips nodes that are already finished (duplicate queue entries)""" + orchestrator = BasicOrchestrator() + + mock_finished_node = MagicMock() + mock_pending_node = MagicMock() + + # Simulate duplicate: add finished node twice, then pending node + orchestrator._execution_queue.put(mock_finished_node) + orchestrator._execution_queue.put(mock_finished_node) # Duplicate! + orchestrator._execution_queue.put(mock_pending_node) + + # Mock nx_graph + mock_nx_graph = mocker.patch( + "qualibrate.orchestration.basic_orchestrator.BasicOrchestrator.nx_graph", + new_callable=PropertyMock, + ) + mock_nx_graph.return_value.pred = { + mock_finished_node: [], + mock_pending_node: [] + } + + # Mock check_node_finished + def check_finished(node): + return node == mock_finished_node + + mocker.patch.object( + orchestrator, "check_node_finished", side_effect=check_finished + ) + + # Should skip both instances of finished_node and return pending_node + result = orchestrator.get_next_element() + assert result == mock_pending_node + + +def test_get_next_element_skips_node_with_unfinished_predecessors(mocker): + """Test that get_next_element skips node if any predecessor is not finished""" + orchestrator = BasicOrchestrator() + + mock_node = MagicMock() + mock_pred1 = MagicMock() + mock_pred2 = MagicMock() + + # Add node to queue + orchestrator._execution_queue.put(mock_node) + + # Mock nx_graph + mock_nx_graph = mocker.patch( + "qualibrate.orchestration.basic_orchestrator.BasicOrchestrator.nx_graph", + new_callable=PropertyMock, + ) + mock_nx_graph.return_value.pred = { + mock_node: [mock_pred1, mock_pred2] + } + + # Mock check_node_finished: pred1 finished, pred2 not finished, node not finished + def check_finished(node): + if node == mock_pred1: + return True + return False + + mocker.patch.object( + orchestrator, "check_node_finished", side_effect=check_finished + ) + + # Should return None because pred2 is not finished + result = orchestrator.get_next_element() + assert result is None + + # Queue should now be empty (node was dequeued but not returned) + assert orchestrator._execution_queue.empty() + + +def test_execute_condition_with_empty_condition_returns_all_targets(): + """Test _execute_condition with empty operational condition (no function, no generator) returns all targets""" + + orchestrator = BasicOrchestrator() + + mock_element = MagicMock() + + # Empty condition (both on_function and on_generator are None) + condition = OperationalCondition() + + targets = ["q1", "q2", "q3"] + result = orchestrator._execute_condition(condition, mock_element, targets) + + # Should return all targets when no condition is specified + assert result == targets + +def test_set_out_targets_with_empty_operational_condition_on_failure(mocker): + """Test that empty operational condition on failed edge passes all failed targets""" + orchestrator = BasicOrchestrator() + + mock_node = MagicMock() + mock_success_successor = MagicMock() + mock_failure_successor = MagicMock() + + # Mock run summary + mock_summary = MagicMock(spec=NodeRunSummary) + mock_summary.successful_targets = ["q1"] + mock_summary.failed_targets = ["q2", "q3"] + mock_node.run_summary = mock_summary + + mock_nx_graph = mocker.patch( + "qualibrate.orchestration.basic_orchestrator.BasicOrchestrator.nx_graph", + new_callable=PropertyMock, + ) + mock_nx_graph.return_value.successors.return_value = [ + mock_success_successor, + mock_failure_successor, + ] + + mock_edges = MagicMock() + + success_edge_data = { + "scenario": Outcome.SUCCESSFUL, + "operational_condition": OperationalCondition() + } + # Empty operational condition - should pass all failed targets + failure_edge_data = { + "scenario": Outcome.FAILED, + "operational_condition": OperationalCondition() # Empty + } + + def edges_getitem(key): + element, successor = key + if successor == mock_success_successor: + return success_edge_data + elif successor == mock_failure_successor: + return failure_edge_data + raise KeyError(key) + + mock_edges.__getitem__.side_effect = edges_getitem + mock_nx_graph.return_value.edges = mock_edges + + orchestrator._graph = MagicMock() + + orchestrator._set_out_targets_for_element(mock_node) + + # Successful edge gets successful targets + assert success_edge_data[QualibrationGraph.EDGE_TARGETS_FIELD] == ["q1"] + + # Failed edge with empty condition should get ALL failed targets + assert set(failure_edge_data[QualibrationGraph.EDGE_TARGETS_FIELD]) == {"q2", "q3"} + + +def test_get_next_element_returns_node_when_all_predecessors_finished( mocker): + """Test that get_next_element returns node when all predecessors are finished""" + orchestrator = BasicOrchestrator() + + mock_node = MagicMock() + mock_pred1 = MagicMock() + mock_pred2 = MagicMock() + + orchestrator._execution_queue.put(mock_node) + + mock_nx_graph = mocker.patch( + "qualibrate.orchestration.basic_orchestrator.BasicOrchestrator.nx_graph", + new_callable=PropertyMock, + ) + mock_nx_graph.return_value.pred = { + mock_node: [mock_pred1, mock_pred2] + } + + def check_finished(node): + if node == mock_node: + return False # The node itself is not finished + return True + + mocker.patch.object( + orchestrator, "check_node_finished", side_effect=check_finished + ) + + # Should return the node because ALL predecessors are finished + result = orchestrator.get_next_element() + assert result == mock_node + + # Queue should now be empty (node was dequeued and returned) + assert orchestrator._execution_queue.empty() + + +def test_execute_condition_with_generator_that_returns_false(): + """Test generator condition that filters out all targets""" + from qualibrate.models.operational_condition import OperationalCondition + + orchestrator = BasicOrchestrator() + mock_element = MagicMock() + + # Generator that always returns False + def never_pass(): + while True: + element, target = yield + yield False + + condition = OperationalCondition(on_generator=never_pass) + targets = ["q1", "q2", "q3"] + result = orchestrator._execute_condition(condition, mock_element, targets) + + assert result == [] + +def test_set_out_targets_multiple_failed_edges_with_different_conditions(mocker): + """Test routing to multiple failed edges with different conditions""" + from qualibrate.models.operational_condition import OperationalCondition + + orchestrator = BasicOrchestrator() + + mock_node = MagicMock() + mock_node.results = { + "q1": {"error_type": "timeout"}, + "q2": {"error_type": "calibration"}, + "q3": {"error_type": "timeout"}, + } + + mock_success_successor = MagicMock() + mock_timeout_handler = MagicMock() + mock_calibration_handler = MagicMock() + + mock_summary = MagicMock(spec=NodeRunSummary) + mock_summary.successful_targets = [] + mock_summary.failed_targets = ["q1", "q2", "q3"] + mock_node.run_summary = mock_summary + + mock_nx_graph = mocker.patch( + "qualibrate.orchestration.basic_orchestrator.BasicOrchestrator.nx_graph", + new_callable=PropertyMock, + ) + mock_nx_graph.return_value.successors.return_value = [ + mock_success_successor, + mock_timeout_handler, + mock_calibration_handler, + ] + + # Different conditions for different error types + timeout_condition = OperationalCondition( + on_function=lambda node, target: node.results[target]["error_type"] == "timeout" + ) + calibration_condition = OperationalCondition( + on_function=lambda node, target: node.results[target]["error_type"] == "calibration" + ) + + mock_edges = MagicMock() + + success_edge_data = { + "scenario": Outcome.SUCCESSFUL, + "operational_condition": OperationalCondition() + } + timeout_edge_data = { + "scenario": Outcome.FAILED, + "operational_condition": timeout_condition + } + calibration_edge_data = { + "scenario": Outcome.FAILED, + "operational_condition": calibration_condition + } + + def edges_getitem(key): + element, successor = key + if successor == mock_success_successor: + return success_edge_data + elif successor == mock_timeout_handler: + return timeout_edge_data + elif successor == mock_calibration_handler: + return calibration_edge_data + raise KeyError(key) + + mock_edges.__getitem__.side_effect = edges_getitem + mock_nx_graph.return_value.edges = mock_edges + + orchestrator._graph = MagicMock() + + #Adding the right parameters to the edge + orchestrator._set_out_targets_for_element(mock_node) + + # Timeout handler should get q1 and q3 + assert set(timeout_edge_data[QualibrationGraph.EDGE_TARGETS_FIELD]) == {"q1", "q3"} + + # Calibration handler should get q2 + assert calibration_edge_data[QualibrationGraph.EDGE_TARGETS_FIELD] == ["q2"] + + # Success edge should be empty + assert success_edge_data[QualibrationGraph.EDGE_TARGETS_FIELD] == [] \ No newline at end of file diff --git a/tests/unit/test_qualibration_graph/test_graph.py b/tests/unit/test_qualibration_graph/test_graph.py index a349685..32cc13c 100644 --- a/tests/unit/test_qualibration_graph/test_graph.py +++ b/tests/unit/test_qualibration_graph/test_graph.py @@ -7,6 +7,7 @@ from qualibrate import QualibrationGraph, QualibrationNode from qualibrate.models.node_status import ElementRunStatus +from qualibrate.models.operational_condition import OperationalCondition from qualibrate.models.outcome import Outcome from qualibrate.models.run_mode import RunModes from qualibrate.parameters import GraphParameters @@ -15,81 +16,6 @@ class TestQualibrationGraph: - @pytest.fixture - def mock_logger(self, mocker): - return mocker.patch("qualibrate.qualibration_graph.logger") - - @pytest.fixture - def mock_library(self, mocker): - """Mock the QualibrationLibrary to avoid initialization issues""" - mock_lib = mocker.MagicMock() - mock_lib.nodes.values_nocopy.return_value = {} # Empty library - mocker.patch.object( - QualibrationGraph, - "_get_library", - return_value=mock_lib, - ) - return mock_lib - - @pytest.fixture - def mock_orchestrator(self, mocker): - return mocker.patch( - "qualibrate.orchestration.basic_orchestrator.QualibrationOrchestrator" - ) - - @pytest.fixture - def pre_setup_graph_nodes(self): - node1 = MagicMock(QualibrationNode) - node1.name = "node1" - node2 = MagicMock(QualibrationNode) - node2.name = "node2" - return {"node1": node1, "node2": node2} - - @pytest.fixture - def mocked__validate_no_elements_from_library(self, mocker): - return mocker.patch( - "qualibrate.qualibration_graph.QualibrationGraph" - "._validate_no_elements_from_library" - ) - - @pytest.fixture - def pre_setup_graph_parameters_build( - self, mocker, mocked__validate_no_elements_from_library - ): - mocked_build_base_parameters = mocker.patch.object( - QRunnable, "build_parameters_class_from_instance" - ) - mocked_build_full_parameters = mocker.patch( - "qualibrate.qualibration_graph.QualibrationGraph" - "._build_parameters_class" - ) - return ( - mocked_build_base_parameters, - mocked_build_full_parameters, - mocked__validate_no_elements_from_library, - ) - - @pytest.fixture - def pre_setup_graph_init( - self, mocker, pre_setup_graph_nodes, pre_setup_graph_parameters_build - ): - mocked_add_nodes_and_connections = mocker.patch( - "qualibrate.qualibration_graph.QualibrationGraph." - "_add_nodes_and_connections_to_nx", - ) - ( - mocked_build_base_parameters, - mocked_build_full_parameters, - mocked__validate_no_elements_from_library, - ) = pre_setup_graph_parameters_build - - return ( - pre_setup_graph_nodes, - mocked_add_nodes_and_connections, - mocked_build_base_parameters, - mocked_build_full_parameters, - ) - def test_init_graph_base( self, mocker, @@ -118,8 +44,9 @@ def test_init_graph_base( assert graph._elements == nodes assert list(graph._connectivity.keys()) == connectivity assert all( - outcome == Outcome.SUCCESSFUL - for outcome in graph._connectivity.values() + edge[QualibrationGraph.RUN_SCENARIO_FIELD] == Outcome.SUCCESSFUL + and edge[QualibrationGraph.OPERATIONAL_CONDITION_FIELD] == OperationalCondition() + for edge in graph._connectivity.values() ) assert graph.name == "test_graph" mocked_build_base_parameters.assert_called_once() @@ -406,9 +333,10 @@ def test_connect_adds_edge_with_successful_outcome( # Check connectivity was added with SUCCESSFUL outcome assert ("node1", "node2") in graph._connectivity - assert graph._connectivity[("node1", "node2")] == Outcome.SUCCESSFUL + assert graph._connectivity[("node1", "node2")][QualibrationGraph.RUN_SCENARIO_FIELD] == Outcome.SUCCESSFUL + assert graph._connectivity[("node1", "node2")][QualibrationGraph.OPERATIONAL_CONDITION_FIELD] == OperationalCondition() - def test_connect_on_failure_adds_edge_with_failed_outcome( + def test_connect_on_failure_adds_edge_with_failed_outcome_without_operational_condition( self, pre_setup_graph_init, mock_library ): """Test that connect_on_failure() adds edge with FAILED outcome""" @@ -423,7 +351,8 @@ def test_connect_on_failure_adds_edge_with_failed_outcome( # Check connectivity was added with FAILED outcome assert ("node1", "node2") in graph._connectivity - assert graph._connectivity[("node1", "node2")] == Outcome.FAILED + assert graph._connectivity[("node1", "node2")][QualibrationGraph.RUN_SCENARIO_FIELD] == Outcome.FAILED + assert graph._connectivity[("node1", "node2")][QualibrationGraph.OPERATIONAL_CONDITION_FIELD] == OperationalCondition() def test_connect_with_node_objects( self, pre_setup_graph_init, mock_library @@ -441,7 +370,8 @@ def test_connect_with_node_objects( graph.connect(node1, node2) assert ("node1", "node2") in graph._connectivity - assert graph._connectivity[("node1", "node2")] == Outcome.SUCCESSFUL + assert graph._connectivity[("node1", "node2")][QualibrationGraph.RUN_SCENARIO_FIELD] == Outcome.SUCCESSFUL + assert graph._connectivity[("node1", "node2")][QualibrationGraph.OPERATIONAL_CONDITION_FIELD] == OperationalCondition() def test_connect_on_failure_with_node_objects( self, pre_setup_graph_init, mock_library @@ -459,7 +389,8 @@ def test_connect_on_failure_with_node_objects( graph.connect_on_failure(node1, node2) assert ("node1", "node2") in graph._connectivity - assert graph._connectivity[("node1", "node2")] == Outcome.FAILED + assert graph._connectivity[("node1", "node2")][QualibrationGraph.RUN_SCENARIO_FIELD] == Outcome.FAILED + assert graph._connectivity[("node1", "node2")][QualibrationGraph.OPERATIONAL_CONDITION_FIELD] == OperationalCondition() def test_connect_raises_error_if_source_not_added( self, pre_setup_graph_nodes, mock_library @@ -499,6 +430,7 @@ def test_connect_raises_error_if_destination_not_added( graph.add_node(nodes["node1"]) graph.connect("node1", "node2") + @pytest.mark.skip def test_connect_idempotent(self, pre_setup_graph_init, mock_library): """Test that calling connect() multiple times on same edge is idempotent""" @@ -514,7 +446,8 @@ def test_connect_idempotent(self, pre_setup_graph_init, mock_library): # Should still only have one edge assert len(graph._connectivity) == 1 - assert graph._connectivity[("node1", "node2")] == Outcome.SUCCESSFUL + assert graph._connectivity[("node1", "node2")][QualibrationGraph.RUN_SCENARIO_FIELD] == Outcome.SUCCESSFUL + assert graph._connectivity[("node1", "node2")][QualibrationGraph.OPERATIONAL_CONDITION_FIELD] == OperationalCondition() def test_mixed_connect_and_connect_on_failure( self, pre_setup_graph_init, mock_library @@ -538,8 +471,11 @@ def test_mixed_connect_and_connect_on_failure( # Check both edges exist with correct outcomes assert len(graph._connectivity) == 2 - assert graph._connectivity[("node1", "node2")] == Outcome.SUCCESSFUL - assert graph._connectivity[("node1", "node3")] == Outcome.FAILED + assert graph._connectivity[("node1", "node2")][QualibrationGraph.RUN_SCENARIO_FIELD] == Outcome.SUCCESSFUL + assert graph._connectivity[("node1", "node2")][QualibrationGraph.OPERATIONAL_CONDITION_FIELD] == OperationalCondition() + assert graph._connectivity[("node1", "node3")][QualibrationGraph.RUN_SCENARIO_FIELD] == Outcome.FAILED + assert graph._connectivity[("node1", "node3")][QualibrationGraph.OPERATIONAL_CONDITION_FIELD] == OperationalCondition() + def test_build_context_manager_finalization( self, pre_setup_graph_init, mock_library @@ -666,7 +602,6 @@ def test__validate_graph_acyclic_without_cycle( ) graph._validate_graph_acyclic() - @pytest.mark.skip def test_connect_then_connect_on_failure_same_edge_overwrites( self, pre_setup_graph_init, mock_library ): @@ -680,8 +615,9 @@ def test_connect_then_connect_on_failure_same_edge_overwrites( ) as graph: graph.add_nodes(nodes["node1"], nodes["node2"]) graph.connect("node1", "node2") # First: SUCCESS - graph.connect_on_failure("node1", "node2") # Then: FAILED + with pytest.raises(ValueError): + graph.connect_on_failure("node1", "node2") # Then: FAILED - # Should only have one edge with FAILED outcome (last write wins) - assert len(graph._connectivity) == 1 - assert graph._connectivity[("node1", "node2")] == Outcome.FAILED + # # Should only have one edge with FAILED outcome (last write wins) + # assert len(graph._connectivity) == 1 + # assert graph._connectivity[("node1", "node2")][QualibrationGraph.RUN_SCENARIO_FIELD] == Outcome.FAILED diff --git a/tests/unit/test_qualibration_graph/test_graph_export_mixin.py b/tests/unit/test_qualibration_graph/test_graph_export_mixin.py index 77889d2..6b2a452 100644 --- a/tests/unit/test_qualibration_graph/test_graph_export_mixin.py +++ b/tests/unit/test_qualibration_graph/test_graph_export_mixin.py @@ -1,4 +1,5 @@ from qualibrate.models.node_status import ElementRunStatus +from qualibrate.models.operational_condition import OperationalCondition from qualibrate.models.outcome import Outcome from qualibrate.parameters import ( GraphParameters, @@ -39,8 +40,8 @@ def test_export( ], # this is standard name so kept as is "adjacency": [ - [{"id": "one_more_node", "scenario": Outcome.SUCCESSFUL}], - [{"id": "test_cal", "scenario": Outcome.SUCCESSFUL}], + [{"id": "one_more_node", QualibrationGraph.RUN_SCENARIO_FIELD: Outcome.SUCCESSFUL, QualibrationGraph.OPERATIONAL_CONDITION_FIELD: OperationalCondition()},], + [{"id": "test_cal", QualibrationGraph.RUN_SCENARIO_FIELD: Outcome.SUCCESSFUL, QualibrationGraph.OPERATIONAL_CONDITION_FIELD: OperationalCondition()}], [], ], } diff --git a/tests/unit/test_qualibration_graph/test_graph_serialize.py b/tests/unit/test_qualibration_graph/test_graph_serialize.py index 4842d21..3bfd8ba 100644 --- a/tests/unit/test_qualibration_graph/test_graph_serialize.py +++ b/tests/unit/test_qualibration_graph/test_graph_serialize.py @@ -155,7 +155,8 @@ def test_serialize_with_nested_graphs( "id": "3->4", "source": 3, "target": 4, - "data": {"condition": True}, + "data": {'connect': True, + 'operational_condition': False,}, } ], }, @@ -168,7 +169,8 @@ def test_serialize_with_nested_graphs( "id": "1->2", "source": 1, "target": 2, - "data": {"condition": True}, + "data": {'connect': True, + 'operational_condition': False,}, } ], } @@ -203,8 +205,8 @@ def test_serialize_with_nested_graphs_and_connect_on_failure( "id": "3->4", "source": 3, "target": 4, - "data": {"condition": False}, - } + "data": {'connect': False, + 'operational_condition': False,}, } ], }, }, @@ -216,7 +218,9 @@ def test_serialize_with_nested_graphs_and_connect_on_failure( "id": "1->2", "source": 1, "target": 2, - "data": {"condition": True}, + "data": {'connect': True, + 'operational_condition': False, + }, } ], } From 0dbb21a002af3f5e554fda27fd8581de22c0f62b Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Wed, 17 Dec 2025 16:21:50 +0200 Subject: [PATCH 20/73] lint --- qualibrate/models/operational_condition.py | 12 +- .../orchestration/basic_orchestrator.py | 44 +++-- qualibrate/qualibration_graph.py | 117 ++++++++---- qualibrate/utils/graph_building.py | 1 - .../test_basic_orchestrator.py | 97 +++++++--- tests/unit/conftest.py | 16 +- .../test_basic_orchestrator.py | 179 ++++++++++++------ .../test_qualibration_graph/test_graph.py | 103 ++++++++-- .../test_graph_export_mixin.py | 16 +- .../test_graph_serialize.py | 26 ++- 10 files changed, 434 insertions(+), 177 deletions(-) diff --git a/qualibrate/models/operational_condition.py b/qualibrate/models/operational_condition.py index 6451c68..831980f 100644 --- a/qualibrate/models/operational_condition.py +++ b/qualibrate/models/operational_condition.py @@ -1,9 +1,9 @@ -from qualibrate.models.outcome import Outcome -from qualibrate.utils.graph_building import ( - GraphElementTypeVar) -from pydantic import BaseModel +from collections.abc import Callable, Generator from typing import Generic -from collections.abc import Generator, Callable + +from pydantic import BaseModel + +from qualibrate.utils.graph_building import GraphElementTypeVar from qualibrate.utils.type_protocols import TargetType @@ -22,4 +22,4 @@ class OperationalCondition(BaseModel, Generic[GraphElementTypeVar]): class LoopCondition(OperationalCondition[GraphElementTypeVar]): max_iterations: int | None = None - on_failure : bool = False + on_failure: bool = False diff --git a/qualibrate/orchestration/basic_orchestrator.py b/qualibrate/orchestration/basic_orchestrator.py index 5498cb8..c21b9a0 100644 --- a/qualibrate/orchestration/basic_orchestrator.py +++ b/qualibrate/orchestration/basic_orchestrator.py @@ -4,14 +4,16 @@ from datetime import datetime from queue import Queue from typing import Generic, cast -from qualibrate.models.operational_condition import OperationalCondition + import networkx as nx + from qualibrate.models.execution_history import ( ExecutionHistoryItem, ItemData, ItemMetadata, ) from qualibrate.models.node_status import ElementRunStatus +from qualibrate.models.operational_condition import OperationalCondition from qualibrate.models.outcome import Outcome from qualibrate.models.run_summary.base import BaseRunSummary from qualibrate.models.run_summary.run_error import RunError @@ -216,7 +218,9 @@ def _set_out_targets_for_element( # self.nx_graph.edges[element, successor]["operational_condition"] is of type OperationalCondition has_on_failed_successors = any( - self.nx_graph.edges[element, successor][QualibrationGraph.RUN_SCENARIO_FIELD] + self.nx_graph.edges[element, successor][ + QualibrationGraph.RUN_SCENARIO_FIELD + ] == Outcome.FAILED for successor in self.nx_graph.successors(element) ) @@ -229,10 +233,17 @@ def _set_out_targets_for_element( QualibrationGraph.EDGE_TARGETS_FIELD ] = ( successful_out_targets - if self.nx_graph.edges[element, successor][QualibrationGraph.RUN_SCENARIO_FIELD] + if self.nx_graph.edges[element, successor][ + QualibrationGraph.RUN_SCENARIO_FIELD + ] == Outcome.SUCCESSFUL - else self._execute_condition(self.nx_graph.edges[element, successor][QualibrationGraph.OPERATIONAL_CONDITION_FIELD], - element, failed_out_targets) + else self._execute_condition( + self.nx_graph.edges[element, successor][ + QualibrationGraph.OPERATIONAL_CONDITION_FIELD + ], + element, + failed_out_targets, + ) ) else: successful_out_targets = ( @@ -246,19 +257,30 @@ def _set_out_targets_for_element( QualibrationGraph.EDGE_TARGETS_FIELD ] = successful_out_targets - def _execute_condition(self, operational_condition: OperationalCondition[GraphElementTypeVar], element: GraphElementTypeVar, targets: list[TargetType]) ->list[TargetType]: + def _execute_condition( + self, + operational_condition: OperationalCondition[GraphElementTypeVar], + element: GraphElementTypeVar, + targets: list[TargetType], + ) -> list[TargetType]: if operational_condition.on_generator is not None: executed_condition = operational_condition.on_generator() - #priming the generator, we need to get to the point where the generator expects out two variables + # priming the generator, we need to get to the point where the generator expects out two variables executed_condition.send(None) - return [target for target in targets if executed_condition.send((element, target))] + return [ + target + for target in targets + if executed_condition.send((element, target)) + ] elif operational_condition.on_function is not None: - return [target for target in targets if operational_condition.on_function(element, target)] + return [ + target + for target in targets + if operational_condition.on_function(element, target) + ] # No condition specified, return all targets return targets - - def _execute_loop_iteration( self, element_to_run: GraphElementTypeVar, diff --git a/qualibrate/qualibration_graph.py b/qualibrate/qualibration_graph.py index 6129b4f..66210e3 100644 --- a/qualibrate/qualibration_graph.py +++ b/qualibrate/qualibration_graph.py @@ -5,7 +5,7 @@ from collections.abc import Callable, Generator, Mapping, Sequence from contextvars import Token from datetime import datetime -from inspect import isgeneratorfunction, Signature +from inspect import isgeneratorfunction from pathlib import Path from types import TracebackType from typing import ( @@ -21,8 +21,11 @@ from typing_extensions import Self from qualibrate.models.node_status import ElementRunStatus +from qualibrate.models.operational_condition import ( + LoopCondition, + OperationalCondition, +) from qualibrate.models.outcome import Outcome -from qualibrate.models.operational_condition import LoopCondition, OperationalCondition from qualibrate.models.run_mode import RunModes from qualibrate.models.run_summary.base import BaseRunSummary from qualibrate.models.run_summary.graph import GraphRunSummary @@ -89,6 +92,7 @@ # Common amount for all loop types (on failure, on function, on generator) # """ + class QualibrationGraph( QRunnable[GraphCreateParametersType, GraphRunParametersType], GraphExportMixin[GraphElementTypeVar], @@ -147,7 +151,7 @@ def __init__( if not isinstance(parameters, GraphParameters): raise ValueError("Graph parameters must be of type GraphParameters") super().__init__(name, parameters, description=description, modes=modes) - self._connectivity: dict[tuple[str, str], dict[str,Any]] + self._connectivity: dict[tuple[str, str], dict[str, Any]] if finalize: if nodes is None or connectivity is None: raise RuntimeError( @@ -157,9 +161,13 @@ def __init__( self._elements = dict(nodes) self._connectivity = { # connectivity: Outcome.SUCCESSFUL - connectivity: {QualibrationGraph.RUN_SCENARIO_FIELD: Outcome.SUCCESSFUL, - QualibrationGraph.OPERATIONAL_CONDITION_FIELD:OperationalCondition[GraphElementTypeVar]()} - for connectivity in connectivity + connectivity: { + QualibrationGraph.RUN_SCENARIO_FIELD: Outcome.SUCCESSFUL, + QualibrationGraph.OPERATIONAL_CONDITION_FIELD: OperationalCondition[ + GraphElementTypeVar + ](), + } + for connectivity in connectivity } else: self._elements = {} @@ -270,8 +278,14 @@ def __copy__(self) -> Self: new_graph._graph.add_edge( new_graph._elements[source], new_graph._elements[destination], - scenario = self._connectivity[(source,destination)][QualibrationGraph.RUN_SCENARIO_FIELD], - operational_condition=copy.deepcopy(self._connectivity[(source, destination)][QualibrationGraph.OPERATIONAL_CONDITION_FIELD]), + scenario=self._connectivity[(source, destination)][ + QualibrationGraph.RUN_SCENARIO_FIELD + ], + operational_condition=copy.deepcopy( + self._connectivity[(source, destination)][ + QualibrationGraph.OPERATIONAL_CONDITION_FIELD + ] + ), ) # Copy orchestrator if it exists @@ -353,8 +367,12 @@ def _add_nodes_and_connections_to_nx(self) -> None: self._graph.add_edge( source_element, destination_element, - scenario = self._connectivity[(source,destination)][QualibrationGraph.RUN_SCENARIO_FIELD], - operational_condition=self._connectivity[(source, destination)][QualibrationGraph.OPERATIONAL_CONDITION_FIELD], + scenario=self._connectivity[(source, destination)][ + QualibrationGraph.RUN_SCENARIO_FIELD + ], + operational_condition=self._connectivity[ + (source, destination) + ][QualibrationGraph.OPERATIONAL_CONDITION_FIELD], ) self._validate_graph_acyclic() @@ -890,15 +908,27 @@ def __serialize_graph_represantation_inner( for adj in adjacency: target_name = adj["id"] - #where QualibrationGraph.OPERATIONAL_CONDITION_FIELD is used, it's because it is how we save the operational condition in the graph - #where it's not used but operational_condition key is still use, it's because this is the key returned to the ui and not necessarily the key we save the data in - #same as for scenario - condition_name, condition_content = self._get_operational_condition_signature_and_content(adj[QualibrationGraph.OPERATIONAL_CONDITION_FIELD]) - operational_condition_data = {'operational_condition': False} + # where QualibrationGraph.OPERATIONAL_CONDITION_FIELD is used, it's because it is how we save the operational condition in the graph + # where it's not used but operational_condition key is still use, it's because this is the key returned to the ui and not necessarily the key we save the data in + # same as for scenario + condition_name, condition_content = ( + self._get_operational_condition_signature_and_content( + adj[QualibrationGraph.OPERATIONAL_CONDITION_FIELD] + ) + ) + operational_condition_data = { + "operational_condition": False + } if condition_name and condition_content: - operational_condition_data["operational_condition"] = True - operational_condition_data["condition_label"] = condition_name - operational_condition_data["condition_description"] = condition_content + operational_condition_data["operational_condition"] = ( + True + ) + operational_condition_data["condition_label"] = ( + condition_name + ) + operational_condition_data["condition_description"] = ( + condition_content + ) flow_dict["edges"].append( { "id": f"{node_id}->" @@ -907,7 +937,8 @@ def __serialize_graph_represantation_inner( "target": name_identifier_dict[target_name], "data": { "connect": adj.get( - QualibrationGraph.RUN_SCENARIO_FIELD, Outcome.SUCCESSFUL + QualibrationGraph.RUN_SCENARIO_FIELD, + Outcome.SUCCESSFUL, ) == Outcome.SUCCESSFUL, **operational_condition_data, @@ -1104,7 +1135,7 @@ def connect( src: str | GraphElementTypeVar, dst: str | GraphElementTypeVar, ) -> None: - self._connect(src=src, dst=dst,run_scenario=Outcome.SUCCESSFUL) + self._connect(src=src, dst=dst, run_scenario=Outcome.SUCCESSFUL) @ensure_not_finalized @ensure_building @@ -1114,18 +1145,16 @@ def connect_on_failure( src: str | GraphElementTypeVar, dst: str | GraphElementTypeVar, on: ( - Callable[ - [], - Generator[ - bool, tuple[GraphElementTypeVar, TargetType] | None, None - ], - ] - | Callable[[GraphElementTypeVar, TargetType], bool] - | None + Callable[ + [], + Generator[ + bool, tuple[GraphElementTypeVar, TargetType] | None, None + ], + ] + | Callable[[GraphElementTypeVar, TargetType], bool] + | None ) = None, - ) -> None: - on_function, on_generator = None, None if on is not None: if isgeneratorfunction(on): @@ -1134,8 +1163,15 @@ def connect_on_failure( on_function = cast( Callable[[GraphElementTypeVar, TargetType], bool], on ) - operational_condition = OperationalCondition(on_function = on_function, on_generator = on_generator) - self._connect(src=src, dst=dst,run_scenario=Outcome.FAILED,operational_condition= operational_condition) + operational_condition = OperationalCondition( + on_function=on_function, on_generator=on_generator + ) + self._connect( + src=src, + dst=dst, + run_scenario=Outcome.FAILED, + operational_condition=operational_condition, + ) @ensure_not_finalized @ensure_building @@ -1153,12 +1189,15 @@ def _connect( f"Both '{s}' and '{d}' must be added before connecting." ) edge = (s, d) - #bug fix it + # bug fix it if edge in self._connectivity: raise ValueError(f"Edge '{edge}' already exists.") - self._connectivity[edge] ={QualibrationGraph.RUN_SCENARIO_FIELD : run_scenario, - QualibrationGraph.OPERATIONAL_CONDITION_FIELD: operational_condition if operational_condition else OperationalCondition()} - + self._connectivity[edge] = { + QualibrationGraph.RUN_SCENARIO_FIELD: run_scenario, + QualibrationGraph.OPERATIONAL_CONDITION_FIELD: operational_condition + if operational_condition + else OperationalCondition(), + } # self._connectivity[edge] = run_scenario @@ -1213,7 +1252,9 @@ def _loop_on_failure( conditions.max_iterations = max_iterations conditions.on_failure = True - def _get_operational_condition_signature_and_content(self,operational_condition) -> tuple[str, str] | tuple[None, None]: + def _get_operational_condition_signature_and_content( + self, operational_condition + ) -> tuple[str, str] | tuple[None, None]: func = None if operational_condition.on_function is not None: func = operational_condition.on_function @@ -1222,4 +1263,4 @@ def _get_operational_condition_signature_and_content(self,operational_condition) if func is not None: return func.__name__, inspect.getsource(func) else: - return None,None \ No newline at end of file + return None, None diff --git a/qualibrate/utils/graph_building.py b/qualibrate/utils/graph_building.py index 562e950..7b91ee0 100644 --- a/qualibrate/utils/graph_building.py +++ b/qualibrate/utils/graph_building.py @@ -27,7 +27,6 @@ R = TypeVar("R") - def ensure_finalized( fn: Callable[Concatenate["QualibrationGraph[Any]", P], R], ) -> Callable[Concatenate["QualibrationGraph[Any]", P], R]: diff --git a/tests/integration/test_orchestration/test_basic_orchestrator.py b/tests/integration/test_orchestration/test_basic_orchestrator.py index 282d402..573ec46 100644 --- a/tests/integration/test_orchestration/test_basic_orchestrator.py +++ b/tests/integration/test_orchestration/test_basic_orchestrator.py @@ -1,28 +1,26 @@ from collections.abc import Generator from pathlib import Path +from unittest.mock import MagicMock import pytest from pydantic import Field +from qualibrate import QualibrationNode from qualibrate.models.execution_history import ( ExecutionHistoryItem, ItemData, ItemMetadata, ) from qualibrate.models.node_status import ElementRunStatus +from qualibrate.models.operational_condition import OperationalCondition from qualibrate.models.outcome import Outcome +from qualibrate.models.run_summary.node import NodeRunSummary from qualibrate.models.run_summary.run_error import RunError from qualibrate.orchestration.basic_orchestrator import BasicOrchestrator -from qualibrate.parameters import ( - GraphParameters, - RunnableParameters -) +from qualibrate.parameters import GraphParameters, RunnableParameters from qualibrate.qualibration_graph import QualibrationGraph from qualibrate.qualibration_library import QualibrationLibrary -from unittest.mock import MagicMock -from qualibrate import QualibrationNode -from qualibrate.models.operational_condition import OperationalCondition -from qualibrate.models.run_summary.node import NodeRunSummary + @pytest.fixture def qualibration_lib( @@ -170,6 +168,7 @@ def test_run_sequence_with_error( ), ) + def test_traverse_graph_with_conditional_failed_edge_filters_targets(): """ Integration test: Verify that operational conditions on failed edges @@ -247,7 +246,8 @@ def test_traverse_graph_with_conditional_failed_edge_filters_targets(): # Operational condition: only retry if error_type is "retriable" retry_condition = OperationalCondition( - on_function=lambda node, target: node.results[target]["error_type"] == "retriable" + on_function=lambda node, target: node.results[target]["error_type"] + == "retriable" ) # Add edges @@ -255,13 +255,13 @@ def test_traverse_graph_with_conditional_failed_edge_filters_targets(): node1, node2_success, scenario=Outcome.SUCCESSFUL, - operational_condition=OperationalCondition() + operational_condition=OperationalCondition(), ) nx_graph.add_edge( node1, node3_retry, scenario=Outcome.FAILED, - operational_condition=retry_condition + operational_condition=retry_condition, ) mock_graph._graph = nx_graph @@ -335,9 +335,18 @@ def node3_run(interactive=False, **kwargs): assert set(execution_log[2][1]) == {"q3", "q4"} # Verify all nodes finished - assert nx_graph.nodes[node1][QualibrationGraph.ELEMENT_STATUS_FIELD] == ElementRunStatus.finished - assert nx_graph.nodes[node2_success][QualibrationGraph.ELEMENT_STATUS_FIELD] == ElementRunStatus.finished - assert nx_graph.nodes[node3_retry][QualibrationGraph.ELEMENT_STATUS_FIELD] == ElementRunStatus.finished + assert ( + nx_graph.nodes[node1][QualibrationGraph.ELEMENT_STATUS_FIELD] + == ElementRunStatus.finished + ) + assert ( + nx_graph.nodes[node2_success][QualibrationGraph.ELEMENT_STATUS_FIELD] + == ElementRunStatus.finished + ) + assert ( + nx_graph.nodes[node3_retry][QualibrationGraph.ELEMENT_STATUS_FIELD] + == ElementRunStatus.finished + ) def test_traverse_graph_with_generator_condition_on_failed_edge(): @@ -424,13 +433,13 @@ def retry_if_not_exhausted(): node1, node2_success, scenario=Outcome.SUCCESSFUL, - operational_condition=OperationalCondition() + operational_condition=OperationalCondition(), ) nx_graph.add_edge( node1, node3_retry, scenario=Outcome.FAILED, - operational_condition=retry_condition + operational_condition=retry_condition, ) mock_graph._graph = nx_graph @@ -504,9 +513,18 @@ def node3_run(interactive=False, **kwargs): assert set(execution_log[2][1]) == {"q2", "q4"} # Verify all nodes finished - assert nx_graph.nodes[node1][QualibrationGraph.ELEMENT_STATUS_FIELD] == ElementRunStatus.finished - assert nx_graph.nodes[node2_success][QualibrationGraph.ELEMENT_STATUS_FIELD] == ElementRunStatus.finished - assert nx_graph.nodes[node3_retry][QualibrationGraph.ELEMENT_STATUS_FIELD] == ElementRunStatus.finished + assert ( + nx_graph.nodes[node1][QualibrationGraph.ELEMENT_STATUS_FIELD] + == ElementRunStatus.finished + ) + assert ( + nx_graph.nodes[node2_success][QualibrationGraph.ELEMENT_STATUS_FIELD] + == ElementRunStatus.finished + ) + assert ( + nx_graph.nodes[node3_retry][QualibrationGraph.ELEMENT_STATUS_FIELD] + == ElementRunStatus.finished + ) def test_traverse_graph_multiple_failed_edges_different_conditions(): @@ -569,7 +587,12 @@ def test_traverse_graph_multiple_failed_edges_different_conditions(): # Setup parameters mock_params = MagicMock() - for name in ["node1", "node2_success", "node3_timeout_handler", "node4_hw_handler"]: + for name in [ + "node1", + "node2_success", + "node3_timeout_handler", + "node4_hw_handler", + ]: params = MagicMock() params.targets = None params.model_dump.return_value = {} @@ -594,9 +617,24 @@ def test_traverse_graph_multiple_failed_edges_different_conditions(): ) # Add edges - nx_graph.add_edge(node1, node2_success, scenario=Outcome.SUCCESSFUL, operational_condition=OperationalCondition()) - nx_graph.add_edge(node1, node3_timeout, scenario=Outcome.FAILED, operational_condition=timeout_condition) - nx_graph.add_edge(node1, node4_hw, scenario=Outcome.FAILED, operational_condition=hw_condition) + nx_graph.add_edge( + node1, + node2_success, + scenario=Outcome.SUCCESSFUL, + operational_condition=OperationalCondition(), + ) + nx_graph.add_edge( + node1, + node3_timeout, + scenario=Outcome.FAILED, + operational_condition=timeout_condition, + ) + nx_graph.add_edge( + node1, + node4_hw, + scenario=Outcome.FAILED, + operational_condition=hw_condition, + ) mock_graph._graph = nx_graph @@ -604,7 +642,7 @@ def test_traverse_graph_multiple_failed_edges_different_conditions(): execution_log = {} def node1_run(interactive=False, **kwargs): - params = getattr(mock_params, "node1") + params = mock_params.node1 targets_received = params.targets[:] execution_log["node1"] = targets_received @@ -619,7 +657,7 @@ def node1_run(interactive=False, **kwargs): return summary def node2_run(interactive=False, **kwargs): - params = getattr(mock_params, "node2_success") + params = mock_params.node2_success targets_received = params.targets[:] execution_log["node2_success"] = targets_received @@ -634,7 +672,7 @@ def node2_run(interactive=False, **kwargs): return summary def node3_run(interactive=False, **kwargs): - params = getattr(mock_params, "node3_timeout_handler") + params = mock_params.node3_timeout_handler targets_received = params.targets[:] execution_log["node3_timeout_handler"] = targets_received @@ -649,7 +687,7 @@ def node3_run(interactive=False, **kwargs): return summary def node4_run(interactive=False, **kwargs): - params = getattr(mock_params, "node4_hw_handler") + params = mock_params.node4_hw_handler targets_received = params.targets[:] execution_log["node4_hw_handler"] = targets_received @@ -679,4 +717,7 @@ def node4_run(interactive=False, **kwargs): # Verify all nodes finished for node in [node1, node2_success, node3_timeout, node4_hw]: - assert nx_graph.nodes[node][QualibrationGraph.ELEMENT_STATUS_FIELD] == ElementRunStatus.finished + assert ( + nx_graph.nodes[node][QualibrationGraph.ELEMENT_STATUS_FIELD] + == ElementRunStatus.finished + ) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index adef161..3c0811a 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -2,17 +2,17 @@ import pytest -from qualibrate import QualibrationNode, QualibrationGraph +from qualibrate import QualibrationGraph, QualibrationNode from qualibrate.q_runnnable import QRunnable @pytest.fixture -def mock_logger( mocker): +def mock_logger(mocker): return mocker.patch("qualibrate.qualibration_graph.logger") @pytest.fixture -def mock_library( mocker): +def mock_library(mocker): """Mock the QualibrationLibrary to avoid initialization issues""" mock_lib = mocker.MagicMock() mock_lib.nodes.values_nocopy.return_value = {} # Empty library @@ -25,7 +25,7 @@ def mock_library( mocker): @pytest.fixture -def mock_orchestrator( mocker): +def mock_orchestrator(mocker): return mocker.patch( "qualibrate.orchestration.basic_orchestrator.QualibrationOrchestrator" ) @@ -41,7 +41,7 @@ def pre_setup_graph_nodes(): @pytest.fixture -def mocked__validate_no_elements_from_library( mocker): +def mocked__validate_no_elements_from_library(mocker): return mocker.patch( "qualibrate.qualibration_graph.QualibrationGraph" "._validate_no_elements_from_library" @@ -50,7 +50,7 @@ def mocked__validate_no_elements_from_library( mocker): @pytest.fixture def pre_setup_graph_parameters_build( - mocker, mocked__validate_no_elements_from_library + mocker, mocked__validate_no_elements_from_library ): mocked_build_base_parameters = mocker.patch.object( QRunnable, "build_parameters_class_from_instance" @@ -68,7 +68,7 @@ def pre_setup_graph_parameters_build( @pytest.fixture def pre_setup_graph_init( - mocker, pre_setup_graph_nodes, pre_setup_graph_parameters_build + mocker, pre_setup_graph_nodes, pre_setup_graph_parameters_build ): mocked_add_nodes_and_connections = mocker.patch( "qualibrate.qualibration_graph.QualibrationGraph." @@ -85,4 +85,4 @@ def pre_setup_graph_init( mocked_add_nodes_and_connections, mocked_build_base_parameters, mocked_build_full_parameters, - ) \ No newline at end of file + ) diff --git a/tests/unit/test_orchestration/test_basic_orchestrator.py b/tests/unit/test_orchestration/test_basic_orchestrator.py index 0de8acf..bdcfc73 100644 --- a/tests/unit/test_orchestration/test_basic_orchestrator.py +++ b/tests/unit/test_orchestration/test_basic_orchestrator.py @@ -9,7 +9,7 @@ from qualibrate.models.outcome import Outcome from qualibrate.models.run_summary.node import NodeRunSummary from qualibrate.orchestration.basic_orchestrator import BasicOrchestrator -from qualibrate.parameters import RunnableParameters, GraphParameters +from qualibrate.parameters import GraphParameters, RunnableParameters from qualibrate.qualibration_graph import QualibrationGraph @@ -139,7 +139,7 @@ def test_get_next_node_returns_node(self, mocker): # Mock check_node_finished to always return True mocker.patch.object( - orchestrator, "check_node_finished", side_effect=[False,True] + orchestrator, "check_node_finished", side_effect=[False, True] ) assert orchestrator.get_next_element() == mock_node @@ -344,11 +344,11 @@ def test_set_out_targets_with_failed_successor(self, mocker): # Create actual dict objects that can be modified success_edge_data = { "scenario": Outcome.SUCCESSFUL, - "operational_condition": OperationalCondition() + "operational_condition": OperationalCondition(), } failure_edge_data = { "scenario": Outcome.FAILED, - "operational_condition": OperationalCondition() # Empty condition + "operational_condition": OperationalCondition(), # Empty condition } def edges_getitem(key): @@ -543,8 +543,18 @@ def test_traverse_graph_with_success_and_failure_paths(self, mocker): ) # Add edges with scenarios - nx_graph.add_edge(node1, node2_success, scenario=Outcome.SUCCESSFUL,operational_condition=OperationalCondition()) - nx_graph.add_edge(node1, node3_failure, scenario=Outcome.FAILED,operational_condition=OperationalCondition()) + nx_graph.add_edge( + node1, + node2_success, + scenario=Outcome.SUCCESSFUL, + operational_condition=OperationalCondition(), + ) + nx_graph.add_edge( + node1, + node3_failure, + scenario=Outcome.FAILED, + operational_condition=OperationalCondition(), + ) mock_graph._graph = nx_graph @@ -682,8 +692,18 @@ def test_traverse_graph_all_targets_succeed_no_failure_path(self, mocker): node3_failure, status=ElementRunStatus.pending, retries=0 ) - nx_graph.add_edge(node1, node2_success, scenario=Outcome.SUCCESSFUL, operational_condition=OperationalCondition()) - nx_graph.add_edge(node1, node3_failure, scenario=Outcome.FAILED, operational_condition=OperationalCondition()) + nx_graph.add_edge( + node1, + node2_success, + scenario=Outcome.SUCCESSFUL, + operational_condition=OperationalCondition(), + ) + nx_graph.add_edge( + node1, + node3_failure, + scenario=Outcome.FAILED, + operational_condition=OperationalCondition(), + ) mock_graph._graph = nx_graph @@ -808,8 +828,18 @@ def test_traverse_graph_success_path_with_no_targets_is_skipped(self): node3_failure, status=ElementRunStatus.pending, retries=0 ) - nx_graph.add_edge(node1, node2_success, scenario=Outcome.SUCCESSFUL, operational_condition=OperationalCondition()) - nx_graph.add_edge(node1, node3_failure, scenario=Outcome.FAILED, operational_condition=OperationalCondition()) + nx_graph.add_edge( + node1, + node2_success, + scenario=Outcome.SUCCESSFUL, + operational_condition=OperationalCondition(), + ) + nx_graph.add_edge( + node1, + node3_failure, + scenario=Outcome.FAILED, + operational_condition=OperationalCondition(), + ) mock_graph._graph = nx_graph @@ -858,7 +888,7 @@ def test_execute_condition_with_lambda_function(): mock_element.results = { "q1": {"value": 10}, "q2": {"value": 5}, - "q3": {"value": 15} + "q3": {"value": 15}, } # Condition: filter targets where value > 8 @@ -871,6 +901,7 @@ def test_execute_condition_with_lambda_function(): assert set(result) == {"q1", "q3"} + def test_execute_condition_with_function(): """Test _execute_condition with a simple function condition""" @@ -880,20 +911,21 @@ def test_execute_condition_with_function(): mock_element.results = { "q1": {"value": 10}, "q2": {"value": 5}, - "q3": {"value": 15} + "q3": {"value": 15}, } + def condition_function(el, target): return el.results[target]["value"] > 8 + # Condition: filter targets where value > 8 - condition = OperationalCondition( - on_function=condition_function - ) + condition = OperationalCondition(on_function=condition_function) targets = ["q1", "q2", "q3"] result = orchestrator._execute_condition(condition, mock_element, targets) assert set(result) == {"q1", "q3"} + def test_execute_condition_with_generator(): """Test _execute_condition with a generator condition""" from qualibrate.models.operational_condition import OperationalCondition @@ -904,7 +936,7 @@ def test_execute_condition_with_generator(): mock_element.results = { "q1": {"fidelity": 0.96}, "q2": {"fidelity": 0.92}, - "q3": {"fidelity": 0.98} + "q3": {"fidelity": 0.98}, } # Generator condition: filter targets where fidelity > 0.95 @@ -921,7 +953,7 @@ def condition_gen(): assert set(result) == {"q1", "q3"} -def test_set_out_targets_with_operational_condition_on_failure( mocker): +def test_set_out_targets_with_operational_condition_on_failure(mocker): """Test _set_out_targets routes targets through operational condition on failed edge""" orchestrator = BasicOrchestrator() @@ -951,10 +983,10 @@ def test_set_out_targets_with_operational_condition_on_failure( mocker): mock_failure_successor, ] - # Operational condition: only route targets with error_count <= 3 op_condition = OperationalCondition( - on_function=lambda node, target: node.results[target]["error_count"] <= 3 + on_function=lambda node, target: node.results[target]["error_count"] + <= 3 ) mock_edges = MagicMock() @@ -962,7 +994,7 @@ def test_set_out_targets_with_operational_condition_on_failure( mocker): success_edge_data = {"scenario": Outcome.SUCCESSFUL} failure_edge_data = { "scenario": Outcome.FAILED, - "operational_condition": op_condition + "operational_condition": op_condition, } def edges_getitem(key): @@ -981,34 +1013,49 @@ def edges_getitem(key): orchestrator._set_out_targets_for_element(mock_node) # Successful edge should get successful targets - assert success_edge_data[QualibrationGraph.EDGE_TARGETS_FIELD] == ["q1", "q2"] + assert success_edge_data[QualibrationGraph.EDGE_TARGETS_FIELD] == [ + "q1", + "q2", + ] # Failed edge should get failed targets filtered by condition (q3 has error_count=2 <= 3) assert failure_edge_data[QualibrationGraph.EDGE_TARGETS_FIELD] == ["q3"] -def test_connect_on_failure_with_condition_lambda_function( pre_setup_graph_init, mock_library): +def test_connect_on_failure_with_condition_lambda_function( + pre_setup_graph_init, mock_library +): """Test that connect_on_failure() with condition function works correctly""" (nodes, _, _, _) = pre_setup_graph_init with QualibrationGraph.build( - name="test_graph", - parameters=GraphParameters(), + name="test_graph", + parameters=GraphParameters(), ) as graph: graph.add_nodes(nodes["node1"], nodes["node2"]) graph.connect_on_failure( - "node1", - "node2", - on=lambda element, target: True + "node1", "node2", on=lambda element, target: True ) # Check connectivity assert ("node1", "node2") in graph._connectivity - assert graph._connectivity[("node1", "node2")][QualibrationGraph.RUN_SCENARIO_FIELD] == Outcome.FAILED - assert graph._connectivity[("node1", "node2")][ - QualibrationGraph.OPERATIONAL_CONDITION_FIELD].on_function is not None + assert ( + graph._connectivity[("node1", "node2")][ + QualibrationGraph.RUN_SCENARIO_FIELD + ] + == Outcome.FAILED + ) + assert ( + graph._connectivity[("node1", "node2")][ + QualibrationGraph.OPERATIONAL_CONDITION_FIELD + ].on_function + is not None + ) -def test_connect_on_failure_with_condition_generator( pre_setup_graph_init, mock_library): + +def test_connect_on_failure_with_condition_generator( + pre_setup_graph_init, mock_library +): """Test that connect_on_failure() with generator condition works correctly""" (nodes, _, _, _) = pre_setup_graph_init @@ -1018,19 +1065,29 @@ def condition_gen(): yield True with QualibrationGraph.build( - name="test_graph", - parameters=GraphParameters(), + name="test_graph", + parameters=GraphParameters(), ) as graph: graph.add_nodes(nodes["node1"], nodes["node2"]) graph.connect_on_failure("node1", "node2", on=condition_gen) # Check connectivity assert ("node1", "node2") in graph._connectivity - assert graph._connectivity[("node1", "node2")][QualibrationGraph.RUN_SCENARIO_FIELD] == Outcome.FAILED - assert graph._connectivity[("node1", "node2")][ - QualibrationGraph.OPERATIONAL_CONDITION_FIELD].on_generator is not None + assert ( + graph._connectivity[("node1", "node2")][ + QualibrationGraph.RUN_SCENARIO_FIELD + ] + == Outcome.FAILED + ) + assert ( + graph._connectivity[("node1", "node2")][ + QualibrationGraph.OPERATIONAL_CONDITION_FIELD + ].on_generator + is not None + ) -def test_get_next_element_skips_already_finished_node( mocker): + +def test_get_next_element_skips_already_finished_node(mocker): """Test that get_next_element skips nodes that are already finished (duplicate queue entries)""" orchestrator = BasicOrchestrator() @@ -1049,7 +1106,7 @@ def test_get_next_element_skips_already_finished_node( mocker): ) mock_nx_graph.return_value.pred = { mock_finished_node: [], - mock_pending_node: [] + mock_pending_node: [], } # Mock check_node_finished @@ -1081,9 +1138,7 @@ def test_get_next_element_skips_node_with_unfinished_predecessors(mocker): "qualibrate.orchestration.basic_orchestrator.BasicOrchestrator.nx_graph", new_callable=PropertyMock, ) - mock_nx_graph.return_value.pred = { - mock_node: [mock_pred1, mock_pred2] - } + mock_nx_graph.return_value.pred = {mock_node: [mock_pred1, mock_pred2]} # Mock check_node_finished: pred1 finished, pred2 not finished, node not finished def check_finished(node): @@ -1119,6 +1174,7 @@ def test_execute_condition_with_empty_condition_returns_all_targets(): # Should return all targets when no condition is specified assert result == targets + def test_set_out_targets_with_empty_operational_condition_on_failure(mocker): """Test that empty operational condition on failed edge passes all failed targets""" orchestrator = BasicOrchestrator() @@ -1146,12 +1202,12 @@ def test_set_out_targets_with_empty_operational_condition_on_failure(mocker): success_edge_data = { "scenario": Outcome.SUCCESSFUL, - "operational_condition": OperationalCondition() + "operational_condition": OperationalCondition(), } # Empty operational condition - should pass all failed targets failure_edge_data = { "scenario": Outcome.FAILED, - "operational_condition": OperationalCondition() # Empty + "operational_condition": OperationalCondition(), # Empty } def edges_getitem(key): @@ -1173,10 +1229,13 @@ def edges_getitem(key): assert success_edge_data[QualibrationGraph.EDGE_TARGETS_FIELD] == ["q1"] # Failed edge with empty condition should get ALL failed targets - assert set(failure_edge_data[QualibrationGraph.EDGE_TARGETS_FIELD]) == {"q2", "q3"} + assert set(failure_edge_data[QualibrationGraph.EDGE_TARGETS_FIELD]) == { + "q2", + "q3", + } -def test_get_next_element_returns_node_when_all_predecessors_finished( mocker): +def test_get_next_element_returns_node_when_all_predecessors_finished(mocker): """Test that get_next_element returns node when all predecessors are finished""" orchestrator = BasicOrchestrator() @@ -1190,9 +1249,7 @@ def test_get_next_element_returns_node_when_all_predecessors_finished( mocker): "qualibrate.orchestration.basic_orchestrator.BasicOrchestrator.nx_graph", new_callable=PropertyMock, ) - mock_nx_graph.return_value.pred = { - mock_node: [mock_pred1, mock_pred2] - } + mock_nx_graph.return_value.pred = {mock_node: [mock_pred1, mock_pred2]} def check_finished(node): if node == mock_node: @@ -1230,7 +1287,10 @@ def never_pass(): assert result == [] -def test_set_out_targets_multiple_failed_edges_with_different_conditions(mocker): + +def test_set_out_targets_multiple_failed_edges_with_different_conditions( + mocker, +): """Test routing to multiple failed edges with different conditions""" from qualibrate.models.operational_condition import OperationalCondition @@ -1264,25 +1324,27 @@ def test_set_out_targets_multiple_failed_edges_with_different_conditions(mocker) # Different conditions for different error types timeout_condition = OperationalCondition( - on_function=lambda node, target: node.results[target]["error_type"] == "timeout" + on_function=lambda node, target: node.results[target]["error_type"] + == "timeout" ) calibration_condition = OperationalCondition( - on_function=lambda node, target: node.results[target]["error_type"] == "calibration" + on_function=lambda node, target: node.results[target]["error_type"] + == "calibration" ) mock_edges = MagicMock() success_edge_data = { "scenario": Outcome.SUCCESSFUL, - "operational_condition": OperationalCondition() + "operational_condition": OperationalCondition(), } timeout_edge_data = { "scenario": Outcome.FAILED, - "operational_condition": timeout_condition + "operational_condition": timeout_condition, } calibration_edge_data = { "scenario": Outcome.FAILED, - "operational_condition": calibration_condition + "operational_condition": calibration_condition, } def edges_getitem(key): @@ -1300,14 +1362,17 @@ def edges_getitem(key): orchestrator._graph = MagicMock() - #Adding the right parameters to the edge + # Adding the right parameters to the edge orchestrator._set_out_targets_for_element(mock_node) # Timeout handler should get q1 and q3 - assert set(timeout_edge_data[QualibrationGraph.EDGE_TARGETS_FIELD]) == {"q1", "q3"} + assert set(timeout_edge_data[QualibrationGraph.EDGE_TARGETS_FIELD]) == { + "q1", + "q3", + } # Calibration handler should get q2 assert calibration_edge_data[QualibrationGraph.EDGE_TARGETS_FIELD] == ["q2"] # Success edge should be empty - assert success_edge_data[QualibrationGraph.EDGE_TARGETS_FIELD] == [] \ No newline at end of file + assert success_edge_data[QualibrationGraph.EDGE_TARGETS_FIELD] == [] diff --git a/tests/unit/test_qualibration_graph/test_graph.py b/tests/unit/test_qualibration_graph/test_graph.py index 32cc13c..2e82239 100644 --- a/tests/unit/test_qualibration_graph/test_graph.py +++ b/tests/unit/test_qualibration_graph/test_graph.py @@ -11,7 +11,6 @@ from qualibrate.models.outcome import Outcome from qualibrate.models.run_mode import RunModes from qualibrate.parameters import GraphParameters -from qualibrate.q_runnnable import QRunnable from qualibrate.utils.exceptions import CyclicGraphError, StopInspection @@ -45,7 +44,8 @@ def test_init_graph_base( assert list(graph._connectivity.keys()) == connectivity assert all( edge[QualibrationGraph.RUN_SCENARIO_FIELD] == Outcome.SUCCESSFUL - and edge[QualibrationGraph.OPERATIONAL_CONDITION_FIELD] == OperationalCondition() + and edge[QualibrationGraph.OPERATIONAL_CONDITION_FIELD] + == OperationalCondition() for edge in graph._connectivity.values() ) assert graph.name == "test_graph" @@ -333,8 +333,18 @@ def test_connect_adds_edge_with_successful_outcome( # Check connectivity was added with SUCCESSFUL outcome assert ("node1", "node2") in graph._connectivity - assert graph._connectivity[("node1", "node2")][QualibrationGraph.RUN_SCENARIO_FIELD] == Outcome.SUCCESSFUL - assert graph._connectivity[("node1", "node2")][QualibrationGraph.OPERATIONAL_CONDITION_FIELD] == OperationalCondition() + assert ( + graph._connectivity[("node1", "node2")][ + QualibrationGraph.RUN_SCENARIO_FIELD + ] + == Outcome.SUCCESSFUL + ) + assert ( + graph._connectivity[("node1", "node2")][ + QualibrationGraph.OPERATIONAL_CONDITION_FIELD + ] + == OperationalCondition() + ) def test_connect_on_failure_adds_edge_with_failed_outcome_without_operational_condition( self, pre_setup_graph_init, mock_library @@ -351,8 +361,18 @@ def test_connect_on_failure_adds_edge_with_failed_outcome_without_operational_co # Check connectivity was added with FAILED outcome assert ("node1", "node2") in graph._connectivity - assert graph._connectivity[("node1", "node2")][QualibrationGraph.RUN_SCENARIO_FIELD] == Outcome.FAILED - assert graph._connectivity[("node1", "node2")][QualibrationGraph.OPERATIONAL_CONDITION_FIELD] == OperationalCondition() + assert ( + graph._connectivity[("node1", "node2")][ + QualibrationGraph.RUN_SCENARIO_FIELD + ] + == Outcome.FAILED + ) + assert ( + graph._connectivity[("node1", "node2")][ + QualibrationGraph.OPERATIONAL_CONDITION_FIELD + ] + == OperationalCondition() + ) def test_connect_with_node_objects( self, pre_setup_graph_init, mock_library @@ -370,8 +390,18 @@ def test_connect_with_node_objects( graph.connect(node1, node2) assert ("node1", "node2") in graph._connectivity - assert graph._connectivity[("node1", "node2")][QualibrationGraph.RUN_SCENARIO_FIELD] == Outcome.SUCCESSFUL - assert graph._connectivity[("node1", "node2")][QualibrationGraph.OPERATIONAL_CONDITION_FIELD] == OperationalCondition() + assert ( + graph._connectivity[("node1", "node2")][ + QualibrationGraph.RUN_SCENARIO_FIELD + ] + == Outcome.SUCCESSFUL + ) + assert ( + graph._connectivity[("node1", "node2")][ + QualibrationGraph.OPERATIONAL_CONDITION_FIELD + ] + == OperationalCondition() + ) def test_connect_on_failure_with_node_objects( self, pre_setup_graph_init, mock_library @@ -389,8 +419,18 @@ def test_connect_on_failure_with_node_objects( graph.connect_on_failure(node1, node2) assert ("node1", "node2") in graph._connectivity - assert graph._connectivity[("node1", "node2")][QualibrationGraph.RUN_SCENARIO_FIELD] == Outcome.FAILED - assert graph._connectivity[("node1", "node2")][QualibrationGraph.OPERATIONAL_CONDITION_FIELD] == OperationalCondition() + assert ( + graph._connectivity[("node1", "node2")][ + QualibrationGraph.RUN_SCENARIO_FIELD + ] + == Outcome.FAILED + ) + assert ( + graph._connectivity[("node1", "node2")][ + QualibrationGraph.OPERATIONAL_CONDITION_FIELD + ] + == OperationalCondition() + ) def test_connect_raises_error_if_source_not_added( self, pre_setup_graph_nodes, mock_library @@ -446,8 +486,18 @@ def test_connect_idempotent(self, pre_setup_graph_init, mock_library): # Should still only have one edge assert len(graph._connectivity) == 1 - assert graph._connectivity[("node1", "node2")][QualibrationGraph.RUN_SCENARIO_FIELD] == Outcome.SUCCESSFUL - assert graph._connectivity[("node1", "node2")][QualibrationGraph.OPERATIONAL_CONDITION_FIELD] == OperationalCondition() + assert ( + graph._connectivity[("node1", "node2")][ + QualibrationGraph.RUN_SCENARIO_FIELD + ] + == Outcome.SUCCESSFUL + ) + assert ( + graph._connectivity[("node1", "node2")][ + QualibrationGraph.OPERATIONAL_CONDITION_FIELD + ] + == OperationalCondition() + ) def test_mixed_connect_and_connect_on_failure( self, pre_setup_graph_init, mock_library @@ -471,11 +521,30 @@ def test_mixed_connect_and_connect_on_failure( # Check both edges exist with correct outcomes assert len(graph._connectivity) == 2 - assert graph._connectivity[("node1", "node2")][QualibrationGraph.RUN_SCENARIO_FIELD] == Outcome.SUCCESSFUL - assert graph._connectivity[("node1", "node2")][QualibrationGraph.OPERATIONAL_CONDITION_FIELD] == OperationalCondition() - assert graph._connectivity[("node1", "node3")][QualibrationGraph.RUN_SCENARIO_FIELD] == Outcome.FAILED - assert graph._connectivity[("node1", "node3")][QualibrationGraph.OPERATIONAL_CONDITION_FIELD] == OperationalCondition() - + assert ( + graph._connectivity[("node1", "node2")][ + QualibrationGraph.RUN_SCENARIO_FIELD + ] + == Outcome.SUCCESSFUL + ) + assert ( + graph._connectivity[("node1", "node2")][ + QualibrationGraph.OPERATIONAL_CONDITION_FIELD + ] + == OperationalCondition() + ) + assert ( + graph._connectivity[("node1", "node3")][ + QualibrationGraph.RUN_SCENARIO_FIELD + ] + == Outcome.FAILED + ) + assert ( + graph._connectivity[("node1", "node3")][ + QualibrationGraph.OPERATIONAL_CONDITION_FIELD + ] + == OperationalCondition() + ) def test_build_context_manager_finalization( self, pre_setup_graph_init, mock_library diff --git a/tests/unit/test_qualibration_graph/test_graph_export_mixin.py b/tests/unit/test_qualibration_graph/test_graph_export_mixin.py index 6b2a452..f37b5a4 100644 --- a/tests/unit/test_qualibration_graph/test_graph_export_mixin.py +++ b/tests/unit/test_qualibration_graph/test_graph_export_mixin.py @@ -40,8 +40,20 @@ def test_export( ], # this is standard name so kept as is "adjacency": [ - [{"id": "one_more_node", QualibrationGraph.RUN_SCENARIO_FIELD: Outcome.SUCCESSFUL, QualibrationGraph.OPERATIONAL_CONDITION_FIELD: OperationalCondition()},], - [{"id": "test_cal", QualibrationGraph.RUN_SCENARIO_FIELD: Outcome.SUCCESSFUL, QualibrationGraph.OPERATIONAL_CONDITION_FIELD: OperationalCondition()}], + [ + { + "id": "one_more_node", + QualibrationGraph.RUN_SCENARIO_FIELD: Outcome.SUCCESSFUL, + QualibrationGraph.OPERATIONAL_CONDITION_FIELD: OperationalCondition(), + }, + ], + [ + { + "id": "test_cal", + QualibrationGraph.RUN_SCENARIO_FIELD: Outcome.SUCCESSFUL, + QualibrationGraph.OPERATIONAL_CONDITION_FIELD: OperationalCondition(), + } + ], [], ], } diff --git a/tests/unit/test_qualibration_graph/test_graph_serialize.py b/tests/unit/test_qualibration_graph/test_graph_serialize.py index 3bfd8ba..777476d 100644 --- a/tests/unit/test_qualibration_graph/test_graph_serialize.py +++ b/tests/unit/test_qualibration_graph/test_graph_serialize.py @@ -155,8 +155,10 @@ def test_serialize_with_nested_graphs( "id": "3->4", "source": 3, "target": 4, - "data": {'connect': True, - 'operational_condition': False,}, + "data": { + "connect": True, + "operational_condition": False, + }, } ], }, @@ -169,8 +171,10 @@ def test_serialize_with_nested_graphs( "id": "1->2", "source": 1, "target": 2, - "data": {'connect': True, - 'operational_condition': False,}, + "data": { + "connect": True, + "operational_condition": False, + }, } ], } @@ -205,8 +209,11 @@ def test_serialize_with_nested_graphs_and_connect_on_failure( "id": "3->4", "source": 3, "target": 4, - "data": {'connect': False, - 'operational_condition': False,}, } + "data": { + "connect": False, + "operational_condition": False, + }, + } ], }, }, @@ -218,9 +225,10 @@ def test_serialize_with_nested_graphs_and_connect_on_failure( "id": "1->2", "source": 1, "target": 2, - "data": {'connect': True, - 'operational_condition': False, - }, + "data": { + "connect": True, + "operational_condition": False, + }, } ], } From 6196ea4a6f6a676e7ef082520d969740351aca53 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Wed, 17 Dec 2025 16:23:40 +0200 Subject: [PATCH 21/73] removed commented code --- qualibrate/qualibration_graph.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/qualibrate/qualibration_graph.py b/qualibrate/qualibration_graph.py index 66210e3..de59a4f 100644 --- a/qualibrate/qualibration_graph.py +++ b/qualibrate/qualibration_graph.py @@ -75,24 +75,6 @@ GraphElLibT = QRunnable[RunnableParameters, RunnableParameters] -# class LoopCondition(BaseModel, Generic[GraphElementTypeVar]): -# on_failure: bool = False -# on_function: Callable[[GraphElementTypeVar, TargetType], bool] | None = None -# on_generator: ( -# Callable[ -# [], -# Generator[ -# bool, tuple[GraphElementTypeVar, TargetType] | None, None -# ], -# ] -# | None -# ) = None -# max_iterations: int | None = None -# """ -# Common amount for all loop types (on failure, on function, on generator) -# """ - - class QualibrationGraph( QRunnable[GraphCreateParametersType, GraphRunParametersType], GraphExportMixin[GraphElementTypeVar], From cb9eb41eb639215046440db64c54b55dfc8361ca Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Wed, 17 Dec 2025 16:26:13 +0200 Subject: [PATCH 22/73] removed commented code --- qualibrate/qualibration_graph.py | 2 -- .../graph_with_loop_condition_lambda.py | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/qualibrate/qualibration_graph.py b/qualibrate/qualibration_graph.py index de59a4f..7cd6bb2 100644 --- a/qualibrate/qualibration_graph.py +++ b/qualibrate/qualibration_graph.py @@ -1181,8 +1181,6 @@ def _connect( else OperationalCondition(), } - # self._connectivity[edge] = run_scenario - def _get_validated_element_name( self, element: str | GraphElementTypeVar ) -> str: diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py index 72f7108..e518bd7 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py @@ -12,8 +12,7 @@ "node", on=lambda node, target: node.results[target]["fidelity"] < 0.95, ) - graph.connect("node", "node2") - # graph.connect_on_failure("node","node2", on=lambda x,y:True) + graph.connect_on_failure("node","node2", on=lambda x,y:True) if __name__ == "__main__": result = graph.run() From f3f349adc41ae63b42eaac59beff4b4110cf1785 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Wed, 17 Dec 2025 19:26:07 +0200 Subject: [PATCH 23/73] changed serialization for ui and fixed broken serialization tests --- qualibrate/qualibration_graph.py | 184 ++++++++++-------- .../test_graph_serialize.py | 64 +++--- 2 files changed, 123 insertions(+), 125 deletions(-) diff --git a/qualibrate/qualibration_graph.py b/qualibrate/qualibration_graph.py index 7cd6bb2..a76de84 100644 --- a/qualibrate/qualibration_graph.py +++ b/qualibrate/qualibration_graph.py @@ -830,107 +830,118 @@ def serialize(self, /, **kwargs: Any) -> Mapping[str, Any]: @ensure_finalized def serialize_graph_representation(self) -> Mapping[str, Any]: - identifier = 1 - - def __serialize_graph_represantation_inner( - graph_self: "QualibrationGraph[Any]", - ) -> Mapping[str, Any]: - flow_dict = defaultdict(list) - nonlocal identifier - nx_data = dict( - graph_self.__class__.nx_graph_export( - graph_self._graph, node_names_only=True - ) + flow_dict = defaultdict(list) + + nx_data = dict( + self.__class__.nx_graph_export( + self._graph, node_names_only=True ) + ) + + nodes_raw = nx_data.pop("nodes") + adj_raw = nx_data.pop("adjacency") - nodes_raw = nx_data.pop("nodes") - adj_raw = nx_data.pop("adjacency") - - name_identifier_dict = {} - for node in nodes_raw: - node_id = identifier - identifier += 1 - node_name = node["id"] - name_identifier_dict[node_name] = node_id - - for node, adjacency in zip(nodes_raw, adj_raw, strict=False): - node_name = node["id"] - node_id = name_identifier_dict[node_name] - - element = graph_self._elements[node_name] - subgraph_data = {} - loop_flag = False - loop_data: dict[str, Any] = {} - node_conditions = self._loop_conditions.get(node_name) - if node_conditions: - loop_flag = True - loop_data["condition"] = ( + for node, adjacency in zip(nodes_raw, adj_raw, strict=False): + node_name = node["id"] + + element = self._elements[node_name] + subgraph_data = {} + loop_flag = False + loop_data: dict[str, Any] = {} + + if node_name in self._loop_conditions: + node_conditions = self._loop_conditions[node_name] + loop_flag = True + loop_data["condition"] = ( node_conditions.on_generator is not None or node_conditions.on_function is not None + ) + loop_data["on_failure"] = node_conditions.on_failure + loop_data["max_iterations"] = node_conditions.max_iterations + + if isinstance(element, QualibrationGraph): + subgraph_data["subgraph"] = ( + element.serialize_graph_representation() + ) + + flow_dict["nodes"].append( + { + "name": node_name, + "loop": loop_flag, + "data": { + "label": node_name, + **subgraph_data, + **loop_data, + }, + } + ) + + for adj in adjacency: + # where QualibrationGraph.OPERATIONAL_CONDITION_FIELD is used, it's because it is how we save the operational condition in the graph + # where it's not used but operational_condition key is still used, it's because this is the key returned to the ui and not necessarily the key we save the data in + # the same as for a scenario + + target_name = adj["id"] + + condition_name, condition_content = ( + self._get_operational_condition_signature_and_content( + adj[QualibrationGraph.OPERATIONAL_CONDITION_FIELD] ) - loop_data["on_failure"] = node_conditions.on_failure - loop_data["max_iterations"] = node_conditions.max_iterations + ) - if isinstance(element, QualibrationGraph): - subgraph_data["subgraph"] = ( - __serialize_graph_represantation_inner(element) + operational_condition_data = { + "operational_condition": False + } + + if condition_name and condition_content: + operational_condition_data.update( + { + "operational_condition": True, + "condition_label": condition_name, + "condition_description": condition_content, + } ) - flow_dict["nodes"].append( + flow_dict["edges"].append( { - "id": node_id, - "loop": loop_flag, + "id": f"{node_name}->{target_name}", + "source": node_name, + "target": target_name, "data": { - "label": node_name, - **subgraph_data, - **loop_data, + "connect_on": adj.get( + QualibrationGraph.RUN_SCENARIO_FIELD, + Outcome.SUCCESSFUL, + ) + == Outcome.SUCCESSFUL, + **operational_condition_data, }, } ) - for adj in adjacency: - target_name = adj["id"] - # where QualibrationGraph.OPERATIONAL_CONDITION_FIELD is used, it's because it is how we save the operational condition in the graph - # where it's not used but operational_condition key is still use, it's because this is the key returned to the ui and not necessarily the key we save the data in - # same as for scenario - condition_name, condition_content = ( - self._get_operational_condition_signature_and_content( - adj[QualibrationGraph.OPERATIONAL_CONDITION_FIELD] - ) - ) - operational_condition_data = { - "operational_condition": False - } - if condition_name and condition_content: - operational_condition_data["operational_condition"] = ( - True - ) - operational_condition_data["condition_label"] = ( - condition_name - ) - operational_condition_data["condition_description"] = ( - condition_content - ) - flow_dict["edges"].append( - { - "id": f"{node_id}->" - f"{name_identifier_dict[target_name]}", - "source": node_id, - "target": name_identifier_dict[target_name], - "data": { - "connect": adj.get( - QualibrationGraph.RUN_SCENARIO_FIELD, - Outcome.SUCCESSFUL, - ) - == Outcome.SUCCESSFUL, - **operational_condition_data, - }, - } - ) + if node_name in self._loop_conditions: + flow_dict["edges"].append( + self._add_loop_to_edge(node_name=node_name) + ) - return dict(flow_dict) + return dict(flow_dict) + + def _add_loop_to_edge(self,node_name): + loop_data: dict[str, Any] = {} + if node_name in self._loop_conditions: + node_conditions = self._loop_conditions.get(node_name) + # loop_flag = True + loop_data["id"] = f'{node_name}->{node_name}' + loop_data["source"] = node_name + loop_data["target"] = node_name + loop_data["data"] = { + "condition": + node_conditions.on_generator is not None + or node_conditions.on_function is not None, + "max_iterations": node_conditions.max_iterations, + } + loop_data["data"]["condition_label"], loop_data["data"]["condition_content"] = self._get_operational_condition_signature_and_content(node_conditions) - return __serialize_graph_represantation_inner(self) + return loop_data def __serialize_data(self, /, **kwargs: Any) -> Mapping[str, Any]: """ @@ -1233,8 +1244,9 @@ def _loop_on_failure( conditions.on_failure = True def _get_operational_condition_signature_and_content( - self, operational_condition + self, operational_condition: OperationalCondition[GraphElementTypeVar] ) -> tuple[str, str] | tuple[None, None]: + # Returns the conditions method signature and also its content func = None if operational_condition.on_function is not None: func = operational_condition.on_function diff --git a/tests/unit/test_qualibration_graph/test_graph_serialize.py b/tests/unit/test_qualibration_graph/test_graph_serialize.py index 777476d..74bc0f0 100644 --- a/tests/unit/test_qualibration_graph/test_graph_serialize.py +++ b/tests/unit/test_qualibration_graph/test_graph_serialize.py @@ -133,30 +133,30 @@ def test_serialize_with_nested_graphs( assert g.serialize_graph_representation() == { "nodes": [ { - "id": 1, + "name": "subg", "loop": False, "data": { "label": "subg", "subgraph": { "nodes": [ { - "id": 3, + "name": "test_cal", "loop": False, "data": {"label": "test_cal"}, }, { - "id": 4, + "name": "one_more_node", "loop": False, "data": {"label": "one_more_node"}, }, ], "edges": [ { - "id": "3->4", - "source": 3, - "target": 4, + "id": "test_cal->one_more_node", + "source": "test_cal", + "target": "one_more_node", "data": { - "connect": True, + "connect_on": True, "operational_condition": False, }, } @@ -164,15 +164,15 @@ def test_serialize_with_nested_graphs( }, }, }, - {"id": 2, "loop": False, "data": {"label": "test_cal"}}, + {"name": "test_cal", "loop": False, "data": {"label": "test_cal"}}, ], "edges": [ { - "id": "1->2", - "source": 1, - "target": 2, + "id": "subg->test_cal", + "source": "subg", + "target": "test_cal", "data": { - "connect": True, + "connect_on": True, "operational_condition": False, }, } @@ -187,48 +187,34 @@ def test_serialize_with_nested_graphs_and_connect_on_failure( assert g.serialize_graph_representation() == { "nodes": [ { - "id": 1, + "name": "subg", "loop": False, "data": { "label": "subg", "subgraph": { "nodes": [ - { - "id": 3, - "loop": False, - "data": {"label": "test_cal"}, - }, - { - "id": 4, - "loop": False, - "data": {"label": "one_more_node"}, - }, + {"name": "test_cal", "loop": False, "data": {"label": "test_cal"}}, + {"name": "one_more_node", "loop": False, "data": {"label": "one_more_node"}}, ], "edges": [ { - "id": "3->4", - "source": 3, - "target": 4, - "data": { - "connect": False, - "operational_condition": False, - }, + "id": "test_cal->one_more_node", + "source": "test_cal", + "target": "one_more_node", + "data": {"connect_on": False, "operational_condition": False}, } ], }, }, }, - {"id": 2, "loop": False, "data": {"label": "test_cal"}}, + {"name": "test_cal", "loop": False, "data": {"label": "test_cal"}}, ], "edges": [ { - "id": "1->2", - "source": 1, - "target": 2, - "data": { - "connect": True, - "operational_condition": False, - }, + "id": "subg->test_cal", + "source": "subg", + "target": "test_cal", + "data": {"connect_on": True, "operational_condition": False}, } ], - } + } \ No newline at end of file From cfac68c823f64f49efa2da06d942a9ed041a2e6d Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Wed, 17 Dec 2025 20:07:39 +0200 Subject: [PATCH 24/73] fixed bug and removed comented code, also removed unnecessary code --- qualibrate/qualibration_graph.py | 39 +++---- .../graph_with_loop_condition_lambda.py | 2 +- ...aph_with_operational_condition_and_loop.py | 48 +++++++++ .../test_graph_serialize.py | 100 +++++++++++++++++- 4 files changed, 160 insertions(+), 29 deletions(-) create mode 100644 tests/unit/test_qualibration_graph/calibrations/graph_with_operational_condition_and_loop.py diff --git a/qualibrate/qualibration_graph.py b/qualibrate/qualibration_graph.py index a76de84..fdf7c2b 100644 --- a/qualibrate/qualibration_graph.py +++ b/qualibrate/qualibration_graph.py @@ -59,6 +59,7 @@ from qualibrate.utils.logger_m import logger from qualibrate.utils.read_files import get_module_name, import_from_path from qualibrate.utils.type_protocols import MachineProtocol, TargetType +from tests.calibration_nodes_and_graphs_for_testing.graph_with_old_building_method import nodes if TYPE_CHECKING: from qualibrate.orchestration.qualibration_orchestrator import ( @@ -833,9 +834,7 @@ def serialize_graph_representation(self) -> Mapping[str, Any]: flow_dict = defaultdict(list) nx_data = dict( - self.__class__.nx_graph_export( - self._graph, node_names_only=True - ) + self.__class__.nx_graph_export(self._graph, node_names_only=True) ) nodes_raw = nx_data.pop("nodes") @@ -849,16 +848,6 @@ def serialize_graph_representation(self) -> Mapping[str, Any]: loop_flag = False loop_data: dict[str, Any] = {} - if node_name in self._loop_conditions: - node_conditions = self._loop_conditions[node_name] - loop_flag = True - loop_data["condition"] = ( - node_conditions.on_generator is not None - or node_conditions.on_function is not None - ) - loop_data["on_failure"] = node_conditions.on_failure - loop_data["max_iterations"] = node_conditions.max_iterations - if isinstance(element, QualibrationGraph): subgraph_data["subgraph"] = ( element.serialize_graph_representation() @@ -889,9 +878,7 @@ def serialize_graph_representation(self) -> Mapping[str, Any]: ) ) - operational_condition_data = { - "operational_condition": False - } + operational_condition_data = {"operational_condition": False} if condition_name and condition_content: operational_condition_data.update( @@ -912,7 +899,7 @@ def serialize_graph_representation(self) -> Mapping[str, Any]: QualibrationGraph.RUN_SCENARIO_FIELD, Outcome.SUCCESSFUL, ) - == Outcome.SUCCESSFUL, + == Outcome.SUCCESSFUL, **operational_condition_data, }, } @@ -925,21 +912,25 @@ def serialize_graph_representation(self) -> Mapping[str, Any]: return dict(flow_dict) - def _add_loop_to_edge(self,node_name): + def _add_loop_to_edge(self, node_name): loop_data: dict[str, Any] = {} if node_name in self._loop_conditions: node_conditions = self._loop_conditions.get(node_name) - # loop_flag = True - loop_data["id"] = f'{node_name}->{node_name}' + loop_data["id"] = f"{node_name}->{node_name}" loop_data["source"] = node_name loop_data["target"] = node_name loop_data["data"] = { - "condition": - node_conditions.on_generator is not None - or node_conditions.on_function is not None, + "condition": node_conditions.on_generator is not None + or node_conditions.on_function is not None + or node_conditions.max_iterations is not None, "max_iterations": node_conditions.max_iterations, } - loop_data["data"]["condition_label"], loop_data["data"]["condition_content"] = self._get_operational_condition_signature_and_content(node_conditions) + ( + loop_data["data"]["condition_label"], + loop_data["data"]["condition_content"], + ) = self._get_operational_condition_signature_and_content( + node_conditions + ) return loop_data diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py index e518bd7..52da00e 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py @@ -12,7 +12,7 @@ "node", on=lambda node, target: node.results[target]["fidelity"] < 0.95, ) - graph.connect_on_failure("node","node2", on=lambda x,y:True) + graph.connect_on_failure("node", "node2", on=lambda x, y: True) if __name__ == "__main__": result = graph.run() diff --git a/tests/unit/test_qualibration_graph/calibrations/graph_with_operational_condition_and_loop.py b/tests/unit/test_qualibration_graph/calibrations/graph_with_operational_condition_and_loop.py new file mode 100644 index 0000000..70324b2 --- /dev/null +++ b/tests/unit/test_qualibration_graph/calibrations/graph_with_operational_condition_and_loop.py @@ -0,0 +1,48 @@ +""" +Graph demonstrating operational conditions on failure edges and loop functionality. + +Graph structure: + node (with loop) - calibration node with retry logic + ├─[SUCCESS]────────────────────> node4 + ├─[FAILED w/ condition True]───> node2 (gets q2, q3, q4) + └─[FAILED w/ condition False]──> node3 (gets nothing) + +Features demonstrated: +- Loop on node with lambda condition checking fidelity +- connect_on_failure with lambda condition (always True) +- connect_on_failure with lambda condition (always False) +- Normal connect for success path +""" + +from qualibrate import QualibrationGraph, QualibrationLibrary + +library = QualibrationLibrary.get_active_library() +USED_NODE = "test_node" + +with QualibrationGraph.build( + "graph_with_operational_condition_and_loop", +) as graph: + graph.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node")) + graph.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node2")) + graph.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node3")) + graph.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node4")) + + # Add loop with fidelity check + graph.loop( + "node", + on=lambda node, target: node.results[target]["fidelity"] < 0.95, + ) + + # Connect failure paths with conditions + # node2: gets all failed targets (condition always True) + graph.connect_on_failure("node", "node2", on=lambda x, y: True) + + # node3: gets no failed targets (condition always False) + graph.connect_on_failure("node", "node3", on=lambda x, y: False) + + # Connect success path + graph.connect("node", "node4") + +if __name__ == "__main__": + result = graph.run(qubits=["q1", "q2", "q3", "q4"]) + print(result) diff --git a/tests/unit/test_qualibration_graph/test_graph_serialize.py b/tests/unit/test_qualibration_graph/test_graph_serialize.py index 74bc0f0..552f33b 100644 --- a/tests/unit/test_qualibration_graph/test_graph_serialize.py +++ b/tests/unit/test_qualibration_graph/test_graph_serialize.py @@ -193,15 +193,26 @@ def test_serialize_with_nested_graphs_and_connect_on_failure( "label": "subg", "subgraph": { "nodes": [ - {"name": "test_cal", "loop": False, "data": {"label": "test_cal"}}, - {"name": "one_more_node", "loop": False, "data": {"label": "one_more_node"}}, + { + "name": "test_cal", + "loop": False, + "data": {"label": "test_cal"}, + }, + { + "name": "one_more_node", + "loop": False, + "data": {"label": "one_more_node"}, + }, ], "edges": [ { "id": "test_cal->one_more_node", "source": "test_cal", "target": "one_more_node", - "data": {"connect_on": False, "operational_condition": False}, + "data": { + "connect_on": False, + "operational_condition": False, + }, } ], }, @@ -217,4 +228,85 @@ def test_serialize_with_nested_graphs_and_connect_on_failure( "data": {"connect_on": True, "operational_condition": False}, } ], - } \ No newline at end of file + } + + +def test_serialize_graph_with_operational_condition_and_loop( + qualibration_lib: QualibrationLibrary, graph_params: GraphParameters +): + """Test serialization of graph with operational conditions and loop.""" + g = qualibration_lib.graphs["graph_with_operational_condition_and_loop"] + + serialized = g.serialize_graph_representation() + + # Should have 4 nodes + assert len(serialized["nodes"]) == 4 + + # Should have 3 edges (1 success, 2 failure) + assert len(serialized["edges"]) == 3 + + # Find node (the one with loop) + node = next(n for n in serialized["nodes"] if n["data"]["label"] == "node") + assert node["loop"] is True + assert node["data"]["condition"] is True # Has loop condition + assert node["data"]["on_failure"] is False # Not a loop on failure + # Note: max_iterations might not be set if only 'on' was specified + + # Find edges + edges = serialized["edges"] + + # Find success edge (node -> node4) + node_id = node["id"] + node4 = next(n for n in serialized["nodes"] if n["data"]["label"] == "node4") + success_edge = next( + e for e in edges + if e["source"] == node_id and e["target"] == node4["id"] + ) + assert success_edge["data"]["connect"] is True # Success path + assert success_edge["data"]["operational_condition"] is False # No condition on success + + # Find failure edges with conditions + node2 = next(n for n in serialized["nodes"] if n["data"]["label"] == "node2") + node2_edge = next( + e for e in edges + if e["source"] == node_id and e["target"] == node2["id"] + ) + assert node2_edge["data"]["connect"] is False # Failure path + assert node2_edge["data"]["operational_condition"] is True # Has condition + assert "condition_label" in node2_edge["data"] + assert node2_edge["data"]["condition_label"] == "" # Lambda function + assert "condition_description" in node2_edge["data"] + + node3 = next(n for n in serialized["nodes"] if n["data"]["label"] == "node3") + node3_edge = next( + e for e in edges + if e["source"] == node_id and e["target"] == node3["id"] + ) + assert node3_edge["data"]["connect"] is False # Failure path + assert node3_edge["data"]["operational_condition"] is True # Has condition + assert "condition_label" in node3_edge["data"] + assert node3_edge["data"]["condition_label"] == "" # Lambda function + + +def test_serialize_graph_with_multiple_operational_conditions( + qualibration_lib: QualibrationLibrary, graph_params: GraphParameters +): + """Test that both failure edges with different conditions are properly serialized.""" + g = qualibration_lib.graphs["graph_with_operational_condition_and_loop"] + + serialized = g.serialize_graph_representation() + edges = serialized["edges"] + + # Count edges by type + success_edges = [e for e in edges if e["data"]["connect"] is True] + failure_edges = [e for e in edges if e["data"]["connect"] is False] + + assert len(success_edges) == 1 + assert len(failure_edges) == 2 + + # Both failure edges should have operational conditions + for edge in failure_edges: + assert edge["data"]["operational_condition"] is True + assert "condition_label" in edge["data"] + assert "condition_description" in edge["data"] + assert edge["data"]["condition_label"] == "" # Both are lambdas From f40abe046f0a250da101eee22f73521c2521f5f5 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Wed, 17 Dec 2025 20:12:49 +0200 Subject: [PATCH 25/73] fixed a bug, unknown import happened --- qualibrate/qualibration_graph.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qualibrate/qualibration_graph.py b/qualibrate/qualibration_graph.py index fdf7c2b..784b866 100644 --- a/qualibrate/qualibration_graph.py +++ b/qualibrate/qualibration_graph.py @@ -59,7 +59,6 @@ from qualibrate.utils.logger_m import logger from qualibrate.utils.read_files import get_module_name, import_from_path from qualibrate.utils.type_protocols import MachineProtocol, TargetType -from tests.calibration_nodes_and_graphs_for_testing.graph_with_old_building_method import nodes if TYPE_CHECKING: from qualibrate.orchestration.qualibration_orchestrator import ( From af162ec3a67b447c8ae97b25c40fad40499395c7 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Sun, 21 Dec 2025 10:06:26 +0200 Subject: [PATCH 26/73] WIP everything is done include testing BUT still need product characterisation to fix the bug with orchestrator and graph on how to decide which target is successfull --- qualibrate/qualibration_graph.py | 2 - .../demo_node.py | 8 +- .../demo_node_that_fails_targets.py | 22 ++++++ .../demo_node_that_succeeds_targets.py | 21 +++++ .../demo_node_with_results.py | 44 +++++++++++ ...aph_with_operational_condition_and_loop.py | 17 ++++- .../calibrations/one_more_test_node.py | 76 +++++++++++++++++++ .../test_graph_serialize.py | 53 ++++++++----- 8 files changed, 216 insertions(+), 27 deletions(-) create mode 100644 tests/calibration_nodes_and_graphs_for_testing/demo_node_that_fails_targets.py create mode 100644 tests/calibration_nodes_and_graphs_for_testing/demo_node_that_succeeds_targets.py create mode 100644 tests/calibration_nodes_and_graphs_for_testing/demo_node_with_results.py create mode 100644 tests/unit/test_qualibration_graph/calibrations/one_more_test_node.py diff --git a/qualibrate/qualibration_graph.py b/qualibrate/qualibration_graph.py index 784b866..7ecef2a 100644 --- a/qualibrate/qualibration_graph.py +++ b/qualibrate/qualibration_graph.py @@ -844,7 +844,6 @@ def serialize_graph_representation(self) -> Mapping[str, Any]: element = self._elements[node_name] subgraph_data = {} - loop_flag = False loop_data: dict[str, Any] = {} if isinstance(element, QualibrationGraph): @@ -855,7 +854,6 @@ def serialize_graph_representation(self) -> Mapping[str, Any]: flow_dict["nodes"].append( { "name": node_name, - "loop": loop_flag, "data": { "label": node_name, **subgraph_data, diff --git a/tests/calibration_nodes_and_graphs_for_testing/demo_node.py b/tests/calibration_nodes_and_graphs_for_testing/demo_node.py index b46f473..43c4ec3 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/demo_node.py +++ b/tests/calibration_nodes_and_graphs_for_testing/demo_node.py @@ -1,19 +1,23 @@ from pydantic import Field from qualibrate import NodeParameters, QualibrationNode +from qualibrate.models.outcome import Outcome class Parameters(NodeParameters): - qubits: list[str] = Field(default_factory=list) + qubits: list[str] = ["q1","q2","q3","q4"] str_value: str = "test" int_value: int = 1 float_value: float = 1.0 node = QualibrationNode("test_node", parameters=Parameters()) -node.parameters = Parameters() +node.parameters = Parameters(qubits=["q1","q2","q3","q4"]) @node.run_action() def node_runs_indication(node: QualibrationNode): node.log("node is running") + node.outcomes["q1"] = "successful" + node.outcomes["q2"] = Outcome.FAILED + return node.outcomes diff --git a/tests/calibration_nodes_and_graphs_for_testing/demo_node_that_fails_targets.py b/tests/calibration_nodes_and_graphs_for_testing/demo_node_that_fails_targets.py new file mode 100644 index 0000000..c8ab05f --- /dev/null +++ b/tests/calibration_nodes_and_graphs_for_testing/demo_node_that_fails_targets.py @@ -0,0 +1,22 @@ +from qualibrate import NodeParameters, QualibrationNode +from qualibrate.models.outcome import Outcome + + +class Parameters(NodeParameters): + qubits: list[str] = ["q1","q2","q3","q4"] + str_value: str = "test" + int_value: int = 1 + float_value: float = 1.0 + + +node = QualibrationNode("demo_node_that_fails_targets", parameters=Parameters()) +# node.parameters = Parameters(qubits=["q1","q2","q3","q4"]) + + +@node.run_action() +def node_runs_indication(node: QualibrationNode): + node.log("node is running") + for target in node.parameters.targets: + node.outcomes[target] = Outcome.FAILED + node.outcomes["q1"] = Outcome.SUCCESSFUL + return node.outcomes diff --git a/tests/calibration_nodes_and_graphs_for_testing/demo_node_that_succeeds_targets.py b/tests/calibration_nodes_and_graphs_for_testing/demo_node_that_succeeds_targets.py new file mode 100644 index 0000000..b64ceed --- /dev/null +++ b/tests/calibration_nodes_and_graphs_for_testing/demo_node_that_succeeds_targets.py @@ -0,0 +1,21 @@ +from qualibrate import NodeParameters, QualibrationNode +from qualibrate.models.outcome import Outcome + + +class Parameters(NodeParameters): + qubits: list[str] = ["q1","q2","q3","q4"] + str_value: str = "test" + int_value: int = 1 + float_value: float = 1.0 + + +node = QualibrationNode("demo_node_that_succeeds_targets", parameters=Parameters()) +# node.parameters = Parameters(qubits=["q1","q2","q3","q4"]) + + +@node.run_action() +def node_runs_indication(node: QualibrationNode): + node.log("node is running") + for target in node.parameters.targets: + node.outcomes[target] = Outcome.SUCCESSFUL + return node.outcomes diff --git a/tests/calibration_nodes_and_graphs_for_testing/demo_node_with_results.py b/tests/calibration_nodes_and_graphs_for_testing/demo_node_with_results.py new file mode 100644 index 0000000..b819872 --- /dev/null +++ b/tests/calibration_nodes_and_graphs_for_testing/demo_node_with_results.py @@ -0,0 +1,44 @@ +from qualibrate import NodeParameters, QualibrationNode +from qualibrate.models.outcome import Outcome + + +class Parameters(NodeParameters): + qubits: list[str] = ["q1","q2","q3","q4"] + str_value: str = "test" + int_value: int = 1 + float_value: float = 1.0 + + +node = QualibrationNode("test_node_with_results", parameters=Parameters()) +node.parameters = Parameters(qubits=["q1","q2","q3","q4"]) + + +# Simulate calibration behavior in node +@node.run_action +def simulate_calibration(node: QualibrationNode): + """Simulate calibration with specific success/failure patterns.""" + results = {} + for target in node.parameters.targets: + if target == "q1": + # q1 succeeds + results[target] = {"fidelity": 0.98, "error_type": None} + # node.outcomes = { + # "q1": "success", + # "fidelity": 0.98, "error_type": None + # } + node.outcomes[target] = Outcome.SUCCESSFUL + elif target == "q2": + # q2 fails with retriable error + results[target] = {"fidelity": 0.92, "error_type": "retriable"} + node.outcomes[target] = Outcome.FAILED + elif target == "q3": + # q3 fails with retriable error + results[target] = {"fidelity": 0.89, "error_type": "retriable"} + node.outcomes[target] = Outcome.FAILED + else: # q4 + # q4 fails with permanent error + results[target] = {"fidelity": 0.85, "error_type": "retriable"} + node.outcomes[target] = Outcome.FAILED + # Store results in namespace so they persist + node.namespace["calibration_results"] = results + return {"calibration_results": results} \ No newline at end of file diff --git a/tests/unit/test_qualibration_graph/calibrations/graph_with_operational_condition_and_loop.py b/tests/unit/test_qualibration_graph/calibrations/graph_with_operational_condition_and_loop.py index 70324b2..ea94453 100644 --- a/tests/unit/test_qualibration_graph/calibrations/graph_with_operational_condition_and_loop.py +++ b/tests/unit/test_qualibration_graph/calibrations/graph_with_operational_condition_and_loop.py @@ -13,14 +13,24 @@ - connect_on_failure with lambda condition (always False) - Normal connect for success path """ +from typing import ClassVar + +from qualibrate import QualibrationGraph, QualibrationLibrary, GraphParameters +from qualibrate.orchestration.basic_orchestrator import BasicOrchestrator + + +class Parameters(GraphParameters): + targets_name: ClassVar[str] = "qubits" + qubits: list[str] -from qualibrate import QualibrationGraph, QualibrationLibrary library = QualibrationLibrary.get_active_library() USED_NODE = "test_node" with QualibrationGraph.build( "graph_with_operational_condition_and_loop", + parameters=Parameters(qubits=[f"q{i}" for i in range(7)]), + orchestrator=BasicOrchestrator(skip_failed=True), ) as graph: graph.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node")) graph.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node2")) @@ -30,7 +40,8 @@ # Add loop with fidelity check graph.loop( "node", - on=lambda node, target: node.results[target]["fidelity"] < 0.95, + on=lambda node, target: target == 'q3', + max_iterations=3 ) # Connect failure paths with conditions @@ -44,5 +55,5 @@ graph.connect("node", "node4") if __name__ == "__main__": - result = graph.run(qubits=["q1", "q2", "q3", "q4"]) + result = graph.run() print(result) diff --git a/tests/unit/test_qualibration_graph/calibrations/one_more_test_node.py b/tests/unit/test_qualibration_graph/calibrations/one_more_test_node.py new file mode 100644 index 0000000..d4df66b --- /dev/null +++ b/tests/unit/test_qualibration_graph/calibrations/one_more_test_node.py @@ -0,0 +1,76 @@ + +""" +Graph demonstrating operational conditions on failure edges and loop functionality. + +Graph structure: + node (with loop) - calibration node with retry logic + ├─[SUCCESS]────────────────────> node4 + ├─[FAILED w/ condition True]───> node2 (gets failed targets) + └─[FAILED w/ condition False]──> node3 (gets no targets) + +Expected behavior with qubits=["q1", "q2", "q3", "q4"]: +- q1: succeeds immediately -> goes to node4 +- q2, q3, q4: fail -> q2 and q3 go to node2 (condition returns True) +- q4: filtered out by condition (returns False) -> goes to node3 but node3's condition blocks it +""" +from typing import ClassVar + +from qualibrate import QualibrationGraph, QualibrationLibrary, QualibrationNode, GraphParameters +from qualibrate.models.outcome import Outcome + +class Parameters(GraphParameters): + targets_name: ClassVar[str] = "qubits" + qubits: list[str] + +library = QualibrationLibrary.get_active_library() +USED_NODE = "test_node_with_results" + +# Create nodes with simulated behavior +with QualibrationGraph.build( + "graph_with_operational_condition_and_loop", + parameters=Parameters(qubits=[f"q{i}" for i in range(1,5)]), + # orchestrator=BasicOrchestrator(skip_failed=True), +) as graph: + node = library.nodes.get_nocopy(USED_NODE).copy(name="node") + node2 = library.nodes.get_nocopy("demo_node_that_fails_targets").copy(name="node2") + node3 = library.nodes.get_nocopy("demo_node_that_fails_targets").copy(name="node3") + node4 = library.nodes.get_nocopy("demo_node_that_succeeds_targets").copy(name="node4") + + graph.add_nodes(node, node2, node3, node4) + + # Add loop with fidelity check (retry if fidelity < 0.95) + # Access results from namespace + graph.loop( + "node", + on=lambda node, target: node.namespace.get("calibration_results").get(target).get("fidelity") < 0.7, + max_iterations=3 + ) + + # Connect failure paths with conditions + # node2: gets retriable errors (error_type == "retriable") + + + graph.connect_on_failure( + "node", + "node2", + on=lambda node, target: node.namespace.get("calibration_results").get(target).get("error_type") == "retriable" + ) + + # node3: gets permanent errors (error_type == "permanent") + graph.connect_on_failure( + "node", + "node3", + # on=lambda node, target: node.namespace.get("calibration_results").get(target).get("error_type") == "permanent" + on= lambda node, target: True + ) + + # Connect success path + graph.connect("node", "node4") + +if __name__ == "__main__": + result = graph.run() + print(f"Run Summary:") + print(f" Initial targets: {result.initial_targets}") + print(f" Successful targets: {result.successful_targets}") + print(f" Failed targets: {result.failed_targets}") + print(f" Outcomes: {result.outcomes}") diff --git a/tests/unit/test_qualibration_graph/test_graph_serialize.py b/tests/unit/test_qualibration_graph/test_graph_serialize.py index 552f33b..b2c8ec0 100644 --- a/tests/unit/test_qualibration_graph/test_graph_serialize.py +++ b/tests/unit/test_qualibration_graph/test_graph_serialize.py @@ -242,47 +242,59 @@ def test_serialize_graph_with_operational_condition_and_loop( # Should have 4 nodes assert len(serialized["nodes"]) == 4 - # Should have 3 edges (1 success, 2 failure) - assert len(serialized["edges"]) == 3 + # Should have 4 edges (1 success, 2 failure, 1 loop self-edge) + assert len(serialized["edges"]) == 4 # Find node (the one with loop) - node = next(n for n in serialized["nodes"] if n["data"]["label"] == "node") - assert node["loop"] is True - assert node["data"]["condition"] is True # Has loop condition - assert node["data"]["on_failure"] is False # Not a loop on failure - # Note: max_iterations might not be set if only 'on' was specified + node_data = next(n for n in serialized["nodes"] if n["data"]["label"] == "node") + node_name = node_data["name"] # Find edges edges = serialized["edges"] + # Find the loop self-edge (node -> node) + loop_edge = next( + (e for e in edges if e["source"] == e["target"]), + None + ) + assert loop_edge is not None + assert loop_edge["id"] == f"{node_name}->{node_name}" + assert loop_edge["data"]["condition"] is True # Has loop condition + assert "condition_label" in loop_edge["data"] + assert loop_edge["data"]["condition_label"] == "" + assert "condition_content" in loop_edge["data"] + assert loop_edge["data"]["max_iterations"] is None # Not specified in this graph + # Find success edge (node -> node4) - node_id = node["id"] - node4 = next(n for n in serialized["nodes"] if n["data"]["label"] == "node4") + node4_data = next(n for n in serialized["nodes"] if n["data"]["label"] == "node4") + node4_name = node4_data["name"] success_edge = next( e for e in edges - if e["source"] == node_id and e["target"] == node4["id"] + if e["source"] == node_name and e["target"] == node4_name ) - assert success_edge["data"]["connect"] is True # Success path + assert success_edge["data"]["connect_on"] is True # Success path assert success_edge["data"]["operational_condition"] is False # No condition on success # Find failure edges with conditions - node2 = next(n for n in serialized["nodes"] if n["data"]["label"] == "node2") + node2_data = next(n for n in serialized["nodes"] if n["data"]["label"] == "node2") + node2_name = node2_data["name"] node2_edge = next( e for e in edges - if e["source"] == node_id and e["target"] == node2["id"] + if e["source"] == node_name and e["target"] == node2_name ) - assert node2_edge["data"]["connect"] is False # Failure path + assert node2_edge["data"]["connect_on"] is False # Failure path assert node2_edge["data"]["operational_condition"] is True # Has condition assert "condition_label" in node2_edge["data"] assert node2_edge["data"]["condition_label"] == "" # Lambda function assert "condition_description" in node2_edge["data"] - node3 = next(n for n in serialized["nodes"] if n["data"]["label"] == "node3") + node3_data = next(n for n in serialized["nodes"] if n["data"]["label"] == "node3") + node3_name = node3_data["name"] node3_edge = next( e for e in edges - if e["source"] == node_id and e["target"] == node3["id"] + if e["source"] == node_name and e["target"] == node3_name ) - assert node3_edge["data"]["connect"] is False # Failure path + assert node3_edge["data"]["connect_on"] is False # Failure path assert node3_edge["data"]["operational_condition"] is True # Has condition assert "condition_label" in node3_edge["data"] assert node3_edge["data"]["condition_label"] == "" # Lambda function @@ -297,9 +309,10 @@ def test_serialize_graph_with_multiple_operational_conditions( serialized = g.serialize_graph_representation() edges = serialized["edges"] - # Count edges by type - success_edges = [e for e in edges if e["data"]["connect"] is True] - failure_edges = [e for e in edges if e["data"]["connect"] is False] + # Count edges by type (excluding self-loop) + non_loop_edges = [e for e in edges if e["source"] != e["target"]] + success_edges = [e for e in non_loop_edges if e["data"]["connect_on"] is True] + failure_edges = [e for e in non_loop_edges if e["data"]["connect_on"] is False] assert len(success_edges) == 1 assert len(failure_edges) == 2 From e60f66284ceb3599f6e75b30c62ccabad3bda31f Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Sun, 21 Dec 2025 11:21:05 +0200 Subject: [PATCH 27/73] WIP, need to solve bug, in order todo that need product characterisation, and solve mypy --- .../demo_node.py | 6 +- .../demo_node_that_fails_targets.py | 2 +- .../demo_node_that_succeeds_targets.py | 6 +- .../demo_node_with_results.py | 6 +- .../test_graph_serialize.py | 73 +++++++++++-------- 5 files changed, 52 insertions(+), 41 deletions(-) diff --git a/tests/calibration_nodes_and_graphs_for_testing/demo_node.py b/tests/calibration_nodes_and_graphs_for_testing/demo_node.py index 43c4ec3..3d1b0a3 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/demo_node.py +++ b/tests/calibration_nodes_and_graphs_for_testing/demo_node.py @@ -1,18 +1,16 @@ -from pydantic import Field - from qualibrate import NodeParameters, QualibrationNode from qualibrate.models.outcome import Outcome class Parameters(NodeParameters): - qubits: list[str] = ["q1","q2","q3","q4"] + qubits: list[str] = ["q1", "q2", "q3", "q4"] str_value: str = "test" int_value: int = 1 float_value: float = 1.0 node = QualibrationNode("test_node", parameters=Parameters()) -node.parameters = Parameters(qubits=["q1","q2","q3","q4"]) +node.parameters = Parameters(qubits=["q1", "q2", "q3", "q4"]) @node.run_action() diff --git a/tests/calibration_nodes_and_graphs_for_testing/demo_node_that_fails_targets.py b/tests/calibration_nodes_and_graphs_for_testing/demo_node_that_fails_targets.py index c8ab05f..0da1436 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/demo_node_that_fails_targets.py +++ b/tests/calibration_nodes_and_graphs_for_testing/demo_node_that_fails_targets.py @@ -3,7 +3,7 @@ class Parameters(NodeParameters): - qubits: list[str] = ["q1","q2","q3","q4"] + qubits: list[str] = ["q1", "q2", "q3", "q4"] str_value: str = "test" int_value: int = 1 float_value: float = 1.0 diff --git a/tests/calibration_nodes_and_graphs_for_testing/demo_node_that_succeeds_targets.py b/tests/calibration_nodes_and_graphs_for_testing/demo_node_that_succeeds_targets.py index b64ceed..0988284 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/demo_node_that_succeeds_targets.py +++ b/tests/calibration_nodes_and_graphs_for_testing/demo_node_that_succeeds_targets.py @@ -3,13 +3,15 @@ class Parameters(NodeParameters): - qubits: list[str] = ["q1","q2","q3","q4"] + qubits: list[str] = ["q1", "q2", "q3", "q4"] str_value: str = "test" int_value: int = 1 float_value: float = 1.0 -node = QualibrationNode("demo_node_that_succeeds_targets", parameters=Parameters()) +node = QualibrationNode( + "demo_node_that_succeeds_targets", parameters=Parameters() +) # node.parameters = Parameters(qubits=["q1","q2","q3","q4"]) diff --git a/tests/calibration_nodes_and_graphs_for_testing/demo_node_with_results.py b/tests/calibration_nodes_and_graphs_for_testing/demo_node_with_results.py index b819872..d926156 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/demo_node_with_results.py +++ b/tests/calibration_nodes_and_graphs_for_testing/demo_node_with_results.py @@ -3,14 +3,14 @@ class Parameters(NodeParameters): - qubits: list[str] = ["q1","q2","q3","q4"] + qubits: list[str] = ["q1", "q2", "q3", "q4"] str_value: str = "test" int_value: int = 1 float_value: float = 1.0 node = QualibrationNode("test_node_with_results", parameters=Parameters()) -node.parameters = Parameters(qubits=["q1","q2","q3","q4"]) +node.parameters = Parameters(qubits=["q1", "q2", "q3", "q4"]) # Simulate calibration behavior in node @@ -41,4 +41,4 @@ def simulate_calibration(node: QualibrationNode): node.outcomes[target] = Outcome.FAILED # Store results in namespace so they persist node.namespace["calibration_results"] = results - return {"calibration_results": results} \ No newline at end of file + return {"calibration_results": results} diff --git a/tests/unit/test_qualibration_graph/test_graph_serialize.py b/tests/unit/test_qualibration_graph/test_graph_serialize.py index b2c8ec0..53a24ab 100644 --- a/tests/unit/test_qualibration_graph/test_graph_serialize.py +++ b/tests/unit/test_qualibration_graph/test_graph_serialize.py @@ -134,19 +134,16 @@ def test_serialize_with_nested_graphs( "nodes": [ { "name": "subg", - "loop": False, "data": { "label": "subg", "subgraph": { "nodes": [ { "name": "test_cal", - "loop": False, "data": {"label": "test_cal"}, }, { "name": "one_more_node", - "loop": False, "data": {"label": "one_more_node"}, }, ], @@ -164,7 +161,7 @@ def test_serialize_with_nested_graphs( }, }, }, - {"name": "test_cal", "loop": False, "data": {"label": "test_cal"}}, + {"name": "test_cal", "data": {"label": "test_cal"}}, ], "edges": [ { @@ -188,19 +185,16 @@ def test_serialize_with_nested_graphs_and_connect_on_failure( "nodes": [ { "name": "subg", - "loop": False, "data": { "label": "subg", "subgraph": { "nodes": [ { "name": "test_cal", - "loop": False, "data": {"label": "test_cal"}, }, { "name": "one_more_node", - "loop": False, "data": {"label": "one_more_node"}, }, ], @@ -218,7 +212,7 @@ def test_serialize_with_nested_graphs_and_connect_on_failure( }, }, }, - {"name": "test_cal", "loop": False, "data": {"label": "test_cal"}}, + {"name": "test_cal", "data": {"label": "test_cal"}}, ], "edges": [ { @@ -232,7 +226,7 @@ def test_serialize_with_nested_graphs_and_connect_on_failure( def test_serialize_graph_with_operational_condition_and_loop( - qualibration_lib: QualibrationLibrary, graph_params: GraphParameters + qualibration_lib: QualibrationLibrary, graph_params: GraphParameters ): """Test serialization of graph with operational conditions and loop.""" g = qualibration_lib.graphs["graph_with_operational_condition_and_loop"] @@ -246,62 +240,75 @@ def test_serialize_graph_with_operational_condition_and_loop( assert len(serialized["edges"]) == 4 # Find node (the one with loop) - node_data = next(n for n in serialized["nodes"] if n["data"]["label"] == "node") + node_data = next( + n for n in serialized["nodes"] if n["data"]["label"] == "node" + ) node_name = node_data["name"] # Find edges edges = serialized["edges"] # Find the loop self-edge (node -> node) - loop_edge = next( - (e for e in edges if e["source"] == e["target"]), - None - ) + loop_edge = next((e for e in edges if e["source"] == e["target"]), None) assert loop_edge is not None assert loop_edge["id"] == f"{node_name}->{node_name}" - assert loop_edge["data"]["condition"] is True # Has loop condition - assert "condition_label" in loop_edge["data"] - assert loop_edge["data"]["condition_label"] == "" - assert "condition_content" in loop_edge["data"] - assert loop_edge["data"]["max_iterations"] is None # Not specified in this graph + # Check for loop-specific fields (adjust based on actual _add_loop_to_edge implementation) + assert "data" in loop_edge + # The loop edge should have max_iterations field + assert "max_iterations" in loop_edge["data"] # Find success edge (node -> node4) - node4_data = next(n for n in serialized["nodes"] if n["data"]["label"] == "node4") + node4_data = next( + n for n in serialized["nodes"] if n["data"]["label"] == "node4" + ) node4_name = node4_data["name"] success_edge = next( - e for e in edges + e + for e in edges if e["source"] == node_name and e["target"] == node4_name ) assert success_edge["data"]["connect_on"] is True # Success path - assert success_edge["data"]["operational_condition"] is False # No condition on success + assert ( + success_edge["data"]["operational_condition"] is False + ) # No condition on success # Find failure edges with conditions - node2_data = next(n for n in serialized["nodes"] if n["data"]["label"] == "node2") + node2_data = next( + n for n in serialized["nodes"] if n["data"]["label"] == "node2" + ) node2_name = node2_data["name"] node2_edge = next( - e for e in edges + e + for e in edges if e["source"] == node_name and e["target"] == node2_name ) assert node2_edge["data"]["connect_on"] is False # Failure path assert node2_edge["data"]["operational_condition"] is True # Has condition assert "condition_label" in node2_edge["data"] - assert node2_edge["data"]["condition_label"] == "" # Lambda function + assert ( + node2_edge["data"]["condition_label"] == "" + ) # Lambda function assert "condition_description" in node2_edge["data"] - node3_data = next(n for n in serialized["nodes"] if n["data"]["label"] == "node3") + node3_data = next( + n for n in serialized["nodes"] if n["data"]["label"] == "node3" + ) node3_name = node3_data["name"] node3_edge = next( - e for e in edges + e + for e in edges if e["source"] == node_name and e["target"] == node3_name ) assert node3_edge["data"]["connect_on"] is False # Failure path assert node3_edge["data"]["operational_condition"] is True # Has condition assert "condition_label" in node3_edge["data"] - assert node3_edge["data"]["condition_label"] == "" # Lambda function + assert ( + node3_edge["data"]["condition_label"] == "" + ) # Lambda function def test_serialize_graph_with_multiple_operational_conditions( - qualibration_lib: QualibrationLibrary, graph_params: GraphParameters + qualibration_lib: QualibrationLibrary, graph_params: GraphParameters ): """Test that both failure edges with different conditions are properly serialized.""" g = qualibration_lib.graphs["graph_with_operational_condition_and_loop"] @@ -311,8 +318,12 @@ def test_serialize_graph_with_multiple_operational_conditions( # Count edges by type (excluding self-loop) non_loop_edges = [e for e in edges if e["source"] != e["target"]] - success_edges = [e for e in non_loop_edges if e["data"]["connect_on"] is True] - failure_edges = [e for e in non_loop_edges if e["data"]["connect_on"] is False] + success_edges = [ + e for e in non_loop_edges if e["data"]["connect_on"] is True + ] + failure_edges = [ + e for e in non_loop_edges if e["data"]["connect_on"] is False + ] assert len(success_edges) == 1 assert len(failure_edges) == 2 From bdcd259d00c2cf617482e337c15e6b2ce2f0b209 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Sun, 21 Dec 2025 11:37:24 +0200 Subject: [PATCH 28/73] fixed all mypy errors, need to lint --- qualibrate/qualibration_graph.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/qualibrate/qualibration_graph.py b/qualibrate/qualibration_graph.py index 7ecef2a..11f4548 100644 --- a/qualibrate/qualibration_graph.py +++ b/qualibrate/qualibration_graph.py @@ -875,7 +875,9 @@ def serialize_graph_representation(self) -> Mapping[str, Any]: ) ) - operational_condition_data = {"operational_condition": False} + operational_condition_data: dict[str, bool | str] = { + "operational_condition": False + } if condition_name and condition_content: operational_condition_data.update( @@ -909,10 +911,10 @@ def serialize_graph_representation(self) -> Mapping[str, Any]: return dict(flow_dict) - def _add_loop_to_edge(self, node_name): + def _add_loop_to_edge(self, node_name: str) -> dict[str, Any]: loop_data: dict[str, Any] = {} if node_name in self._loop_conditions: - node_conditions = self._loop_conditions.get(node_name) + node_conditions = self._loop_conditions[node_name] loop_data["id"] = f"{node_name}->{node_name}" loop_data["source"] = node_name loop_data["target"] = node_name @@ -1144,8 +1146,10 @@ def connect_on_failure( on_function = cast( Callable[[GraphElementTypeVar, TargetType], bool], on ) - operational_condition = OperationalCondition( - on_function=on_function, on_generator=on_generator + operational_condition: OperationalCondition[GraphElementTypeVar] = ( + OperationalCondition( + on_function=on_function, on_generator=on_generator + ) ) self._connect( src=src, @@ -1161,7 +1165,8 @@ def _connect( src: str | GraphElementTypeVar, dst: str | GraphElementTypeVar, run_scenario: Outcome, - operational_condition: OperationalCondition[GraphElementTypeVar] = None, + operational_condition: OperationalCondition[GraphElementTypeVar] + | None = None, ) -> None: s = self._resolve_element_name(src) d = self._resolve_element_name(dst) @@ -1235,12 +1240,12 @@ def _get_operational_condition_signature_and_content( self, operational_condition: OperationalCondition[GraphElementTypeVar] ) -> tuple[str, str] | tuple[None, None]: # Returns the conditions method signature and also its content - func = None if operational_condition.on_function is not None: func = operational_condition.on_function - elif operational_condition.on_generator is not None: - func = operational_condition.on_generator - if func is not None: return func.__name__, inspect.getsource(func) - else: - return None, None + + if operational_condition.on_generator is not None: + gen = operational_condition.on_generator + return gen.__name__, inspect.getsource(gen) + + return None, None From 6b7ac9b9bdeb38ea070409827bac0b36e8218950 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Mon, 22 Dec 2025 10:25:43 +0200 Subject: [PATCH 29/73] changed graphs names --- .../graph_with_loop_condition_function.py | 2 +- .../graph_with_loop_condition_generatior.py | 2 +- .../graph_with_loop_condition_lambda.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_function.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_function.py index 1ec1e6f..df1fe67 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_function.py +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_function.py @@ -9,7 +9,7 @@ def check_fidelity(node: QualibrationNode, target: str): with QualibrationGraph.build( - "graph_with_loop", + "graph_with_loop_with_function", ) as graph: graph.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node")) graph.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node2")) diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_generatior.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_generatior.py index f1839c8..ac51961 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_generatior.py +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_generatior.py @@ -10,7 +10,7 @@ def check_fidelity(node: QualibrationNode, target: str): with QualibrationGraph.build( - "graph_with_loop", + "graph_with_loop_with_generator", ) as graph: graph.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node")) graph.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node2")) diff --git a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py index 52da00e..1f76279 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py +++ b/tests/calibration_nodes_and_graphs_for_testing/graph_with_loop_condition_lambda.py @@ -4,7 +4,7 @@ USED_NODE = "test_node" with QualibrationGraph.build( - "graph_with_loop", + "graph_with_loop_with_lambda", ) as graph: graph.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node")) graph.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node2")) From 7f6d5da3372654cc655ea0f89d426a1b1e567fd6 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Mon, 22 Dec 2025 13:43:13 +0200 Subject: [PATCH 30/73] fixed schema for ui, still need product to give characterisation for orchestrator bug --- qualibrate/qualibration_graph.py | 67 +++++++++---------- .../test_graph_serialize.py | 38 +++++------ 2 files changed, 48 insertions(+), 57 deletions(-) diff --git a/qualibrate/qualibration_graph.py b/qualibrate/qualibration_graph.py index 11f4548..353dd58 100644 --- a/qualibrate/qualibration_graph.py +++ b/qualibrate/qualibration_graph.py @@ -866,7 +866,6 @@ def serialize_graph_representation(self) -> Mapping[str, Any]: # where QualibrationGraph.OPERATIONAL_CONDITION_FIELD is used, it's because it is how we save the operational condition in the graph # where it's not used but operational_condition key is still used, it's because this is the key returned to the ui and not necessarily the key we save the data in # the same as for a scenario - target_name = adj["id"] condition_name, condition_content = ( @@ -875,32 +874,33 @@ def serialize_graph_representation(self) -> Mapping[str, Any]: ) ) - operational_condition_data: dict[str, bool | str] = { - "operational_condition": False - } + operational_condition_data: dict[str, bool | str] = {} + if condition_name and condition_content: operational_condition_data.update( { - "operational_condition": True, - "condition_label": condition_name, - "condition_description": condition_content, + "label": condition_name, + "description": condition_content, } ) + data = { + "connect_on": adj.get( + QualibrationGraph.RUN_SCENARIO_FIELD, + Outcome.SUCCESSFUL, + ) + == Outcome.SUCCESSFUL, + } + if operational_condition_data: + data["condition"] = operational_condition_data + flow_dict["edges"].append( { "id": f"{node_name}->{target_name}", "source": node_name, "target": target_name, - "data": { - "connect_on": adj.get( - QualibrationGraph.RUN_SCENARIO_FIELD, - Outcome.SUCCESSFUL, - ) - == Outcome.SUCCESSFUL, - **operational_condition_data, - }, + "data": data } ) @@ -912,26 +912,23 @@ def serialize_graph_representation(self) -> Mapping[str, Any]: return dict(flow_dict) def _add_loop_to_edge(self, node_name: str) -> dict[str, Any]: - loop_data: dict[str, Any] = {} - if node_name in self._loop_conditions: - node_conditions = self._loop_conditions[node_name] - loop_data["id"] = f"{node_name}->{node_name}" - loop_data["source"] = node_name - loop_data["target"] = node_name - loop_data["data"] = { - "condition": node_conditions.on_generator is not None - or node_conditions.on_function is not None - or node_conditions.max_iterations is not None, - "max_iterations": node_conditions.max_iterations, - } - ( - loop_data["data"]["condition_label"], - loop_data["data"]["condition_content"], - ) = self._get_operational_condition_signature_and_content( - node_conditions - ) - - return loop_data + if node_name not in self._loop_conditions: + raise KeyError(f"Loop condition not found for node {node_name}") + + node_conditions: LoopCondition[GraphElementTypeVar] = self._loop_conditions[node_name] + label,content = self._get_operational_condition_signature_and_content(node_conditions) + data = {'loop':{}} + if label and content: + data["loop"]["label"] = label + data["loop"]["content"] = content + if max_iterations := node_conditions.max_iterations: + data["loop"]["max_iterations"] = max_iterations + return { + "id": f"{node_name}->{node_name}", + "source": node_name, + "target": node_name, + "data": data, + } def __serialize_data(self, /, **kwargs: Any) -> Mapping[str, Any]: """ diff --git a/tests/unit/test_qualibration_graph/test_graph_serialize.py b/tests/unit/test_qualibration_graph/test_graph_serialize.py index 53a24ab..6141bc9 100644 --- a/tests/unit/test_qualibration_graph/test_graph_serialize.py +++ b/tests/unit/test_qualibration_graph/test_graph_serialize.py @@ -130,6 +130,7 @@ def test_serialize_with_nested_graphs( qualibration_lib: QualibrationLibrary, graph_params: GraphParameters ): g = qualibration_lib.graphs["workflow_top"] + assert g.serialize_graph_representation() == { "nodes": [ { @@ -154,7 +155,6 @@ def test_serialize_with_nested_graphs( "target": "one_more_node", "data": { "connect_on": True, - "operational_condition": False, }, } ], @@ -170,17 +170,16 @@ def test_serialize_with_nested_graphs( "target": "test_cal", "data": { "connect_on": True, - "operational_condition": False, }, } ], } - def test_serialize_with_nested_graphs_and_connect_on_failure( qualibration_lib: QualibrationLibrary, graph_params: GraphParameters ): g = qualibration_lib.graphs["workflow_top_connect_on_failure"] + assert g.serialize_graph_representation() == { "nodes": [ { @@ -205,7 +204,6 @@ def test_serialize_with_nested_graphs_and_connect_on_failure( "target": "one_more_node", "data": { "connect_on": False, - "operational_condition": False, }, } ], @@ -219,7 +217,9 @@ def test_serialize_with_nested_graphs_and_connect_on_failure( "id": "subg->test_cal", "source": "subg", "target": "test_cal", - "data": {"connect_on": True, "operational_condition": False}, + "data": { + "connect_on": True, + }, } ], } @@ -255,7 +255,8 @@ def test_serialize_graph_with_operational_condition_and_loop( # Check for loop-specific fields (adjust based on actual _add_loop_to_edge implementation) assert "data" in loop_edge # The loop edge should have max_iterations field - assert "max_iterations" in loop_edge["data"] + assert "loop" in loop_edge["data"] + assert "max_iterations" in loop_edge["data"]["loop"] # Find success edge (node -> node4) node4_data = next( @@ -268,9 +269,6 @@ def test_serialize_graph_with_operational_condition_and_loop( if e["source"] == node_name and e["target"] == node4_name ) assert success_edge["data"]["connect_on"] is True # Success path - assert ( - success_edge["data"]["operational_condition"] is False - ) # No condition on success # Find failure edges with conditions node2_data = next( @@ -283,12 +281,9 @@ def test_serialize_graph_with_operational_condition_and_loop( if e["source"] == node_name and e["target"] == node2_name ) assert node2_edge["data"]["connect_on"] is False # Failure path - assert node2_edge["data"]["operational_condition"] is True # Has condition - assert "condition_label" in node2_edge["data"] - assert ( - node2_edge["data"]["condition_label"] == "" - ) # Lambda function - assert "condition_description" in node2_edge["data"] + assert "condition" in node2_edge["data"] + assert node2_edge["data"]["condition"]["label"] == "" + assert "description" in node2_edge["data"]["condition"] node3_data = next( n for n in serialized["nodes"] if n["data"]["label"] == "node3" @@ -300,10 +295,9 @@ def test_serialize_graph_with_operational_condition_and_loop( if e["source"] == node_name and e["target"] == node3_name ) assert node3_edge["data"]["connect_on"] is False # Failure path - assert node3_edge["data"]["operational_condition"] is True # Has condition - assert "condition_label" in node3_edge["data"] + assert "label" in node3_edge["data"]['condition'] assert ( - node3_edge["data"]["condition_label"] == "" + node3_edge["data"]['condition']["label"] == "" ) # Lambda function @@ -330,7 +324,7 @@ def test_serialize_graph_with_multiple_operational_conditions( # Both failure edges should have operational conditions for edge in failure_edges: - assert edge["data"]["operational_condition"] is True - assert "condition_label" in edge["data"] - assert "condition_description" in edge["data"] - assert edge["data"]["condition_label"] == "" # Both are lambdas + # assert edge["data"]["operational_condition"] is True + assert "label" in edge["data"]['condition'] + assert "description" in edge["data"]['condition'] + assert edge["data"]['condition']["description"] is not None From 2a0ea9067f364522c369b7358d8d22caf4de1bf6 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Tue, 23 Dec 2025 15:52:11 +0200 Subject: [PATCH 31/73] added enums, need to add tests --- qualibrate/utils/parameters.py | 12 +- .../demo_node.py | 15 +- .../test_execution_parameters.py | 448 ++++++++++++++---- 3 files changed, 371 insertions(+), 104 deletions(-) diff --git a/qualibrate/utils/parameters.py b/qualibrate/utils/parameters.py index d36739c..d856314 100644 --- a/qualibrate/utils/parameters.py +++ b/qualibrate/utils/parameters.py @@ -29,7 +29,13 @@ def recursive_properties_solver( structure.update(structure.pop("allOf")[0]) if "$ref" in structure: resolved = resolve_pointer(schema, structure["$ref"][1:]) - properties[name] = recursive_properties_solver( - resolved["properties"], schema - ) + if "properties" in resolved: + properties[name] = recursive_properties_solver( + resolved["properties"], schema + ) + else: + properties[name] = resolved + if 'default' in structure: + properties[name]["default"] = structure["default"] return properties + diff --git a/tests/calibration_nodes_and_graphs_for_testing/demo_node.py b/tests/calibration_nodes_and_graphs_for_testing/demo_node.py index b46f473..f4544e2 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/demo_node.py +++ b/tests/calibration_nodes_and_graphs_for_testing/demo_node.py @@ -1,17 +1,26 @@ +from typing import Optional + from pydantic import Field from qualibrate import NodeParameters, QualibrationNode +from enum import Enum + +from qualibrate.models.outcome import Outcome +class Color(Enum): + RED = "red" + GREEN = "green" + BLUE = "blue" + class Parameters(NodeParameters): qubits: list[str] = Field(default_factory=list) str_value: str = "test" int_value: int = 1 - float_value: float = 1.0 - + float_value: float = 1 + random_color: Color = Color.BLUE node = QualibrationNode("test_node", parameters=Parameters()) -node.parameters = Parameters() @node.run_action() diff --git a/tests/unit/test_parameters/test_execution_parameters.py b/tests/unit/test_parameters/test_execution_parameters.py index 739852c..75f0dba 100644 --- a/tests/unit/test_parameters/test_execution_parameters.py +++ b/tests/unit/test_parameters/test_execution_parameters.py @@ -1,3 +1,6 @@ +from email.policy import default +from enum import Enum + import pytest from pydantic import Field @@ -10,6 +13,10 @@ ) from qualibrate.utils.type_protocols import TargetType +class Color(Enum): + RED = "Red" + GREEN = "Green" + BLUE = "Blue" class Node1(NodeParameters): qubits: list[TargetType] | None = Field( @@ -24,6 +31,12 @@ class Node2(NodeParameters): ) float_value: float = 2.0 +class Node3(NodeParameters): + qubits: list[TargetType] | None = Field( + default_factory=lambda: ["d", "e", "f"] + ) + color: Color = Color.RED + class Graph(GraphParameters): qubits: list[TargetType] = Field(default_factory=lambda: ["1", "2", "3"]) @@ -91,116 +104,355 @@ def test_serialize_exclude_targets_default(self): }, } - def test_serialize_force_exclude_targets(self): - assert ExecutionParams.serialize(exclude_targets=True) == { - "parameters": { - "str_value": { - "default": "test", - "title": "Str Value", - "type": "string", + def test_serialize_force_exclude_targets(self): + assert ExecutionParams.serialize(exclude_targets=True) == { + "parameters": { + "str_value": { + "default": "test", + "title": "Str Value", + "type": "string", + "is_targets": False, + } + }, + "nodes": { + "node1": { + "int_value": { + "default": 1, + "title": "Int Value", + "type": "integer", "is_targets": False, } }, - "nodes": { - "node1": { - "int_value": { - "default": 1, - "title": "Int Value", - "type": "integer", - "is_targets": False, - } - }, - "node2": { - "float_value": { - "default": 2.0, - "title": "Float Value", - "type": "number", - "is_targets": False, - } - }, + "node2": { + "float_value": { + "default": 2.0, + "title": "Float Value", + "type": "number", + "is_targets": False, + } }, - } + }, + } - def test_serialize_force_not_exclude_targets(self): - assert ExecutionParams.serialize(exclude_targets=False) == { - "parameters": { + def test_serialize_force_not_exclude_targets(self): + assert ExecutionParams.serialize(exclude_targets=False) == { + "parameters": { + "qubits": { + "items": {"type": "string"}, + "type": "array", + "title": "Qubits", + "is_targets": True, + }, + "str_value": { + "default": "test", + "title": "Str Value", + "type": "string", + "is_targets": False, + }, + }, + "nodes": { + "node1": { "qubits": { - "items": {"type": "string"}, - "type": "array", + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + # "default": ["a", "b", "c"], "title": "Qubits", "is_targets": True, }, - "str_value": { - "default": "test", - "title": "Str Value", - "type": "string", + "int_value": { + "default": 1, + "title": "Int Value", + "type": "integer", "is_targets": False, }, }, - "nodes": { - "node1": { - "qubits": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "default": ["a", "b", "c"], - "title": "Qubits", - "is_targets": True, - }, - "int_value": { - "default": 1, - "title": "Int Value", - "type": "integer", - "is_targets": False, - }, + "node2": { + "qubits": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + # "default": ["d", "e", "f"], + "title": "Qubits", + "is_targets": True, }, - "node2": { - "qubits": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "default": ["d", "e", "f"], - "title": "Qubits", - "is_targets": True, - }, - "float_value": { - "default": 2.0, - "title": "Float Value", - "type": "number", - "is_targets": False, - }, + "float_value": { + "default": 2.0, + "title": "Float Value", + "type": "number", + "is_targets": False, }, }, - } - - def test_serialize_with_none_parameters_class(self, mocker): - mock_model_fields = mocker.patch( - "qualibrate.parameters.ExecutionParameters.model_fields" - ) - mock_model_fields.__getitem__.return_value = mocker.MagicMock( - annotation=None - ) - - with pytest.raises( - RuntimeError, match="Graph parameters class can't be none" - ): - ExecutionParameters.serialize() - - def test_serialize_none_parameters_class(self, mocker): - mock_model_fields = mocker.patch( - "qualibrate.parameters.ExecutionParameters.model_fields" - ) - mock_model_fields.__getitem__.return_value = mocker.MagicMock( - annotation=RunnableParameters - ) - - with pytest.raises( - RuntimeError, - match=( - "Graph parameters class should be subclass of " - "qualibrate.parameters.GraphParameters" - ), - ): - ExecutionParameters.serialize() + }, + } + + def test_serialize_with_none_parameters_class(self, mocker): + mock_model_fields = mocker.patch( + "qualibrate.parameters.ExecutionParameters.model_fields" + ) + mock_model_fields.__getitem__.return_value = mocker.MagicMock( + annotation=None + ) + + with pytest.raises( + RuntimeError, match="Graph parameters class can't be none" + ): + ExecutionParameters.serialize() + + def test_serialize_none_parameters_class(self, mocker): + mock_model_fields = mocker.patch( + "qualibrate.parameters.ExecutionParameters.model_fields" + ) + mock_model_fields.__getitem__.return_value = mocker.MagicMock( + annotation=RunnableParameters + ) + + with pytest.raises( + RuntimeError, + match=( + "Graph parameters class should be subclass of " + "qualibrate.parameters.GraphParameters" + ), + ): + ExecutionParameters.serialize() + +# +# class Color(Enum): +# RED = "red" +# GREEN = "green" +# BLUE = "blue" +# +# +# class Priority(Enum): +# LOW = 1 +# MEDIUM = 2 +# HIGH = 3 +# +# +# class ComprehensiveParameters(NodeParameters): +# """Test all parameter types that should be supported""" +# # Required for NodeParameters +# qubits: list[str] = Field(default_factory=list) +# +# # Simple types (should already work) +# str_value: str = "test" +# int_value: int = 1 +# float_value: float = 1.0 +# bool_value: bool = True +# none_value: Optional[str] = None +# +# # Collections (should already work) +# list_value: list[int] = Field(default_factory=lambda: [1, 2, 3]) +# dict_value: dict[str, int] = Field(default_factory=lambda: {"a": 1}) +# +# # Enums (the new case we're fixing) +# color_enum: Color = Color.RED +# priority_enum: Priority = Priority.MEDIUM +# optional_enum: Optional[Color] = None +# +# +# def test_node_serialization(): +# """Test that node with comprehensive parameters can be created and serialized""" +# print("=" * 80) +# print("TEST 1: Node Creation and Serialization") +# print("=" * 80) +# +# try: +# # Create node with comprehensive parameters +# node = QualibrationNode("test_node", parameters=ComprehensiveParameters()) +# print("✓ Node created successfully") +# +# # Test serialization +# serialized = node.serialize() +# print("✓ Node serialized successfully") +# print(f" Serialized keys: {list(serialized.keys())}") +# +# # Check parameters are in serialized output +# assert "parameters" in serialized, "Missing 'parameters' in serialized output" +# params = serialized["parameters"] +# print(f" Parameter keys: {list(params.keys())}") +# +# # Verify all fields are present +# expected_fields = [ +# "qubits", "str_value", "int_value", "float_value", "bool_value", +# "none_value", "list_value", "dict_value", "color_enum", +# "priority_enum", "optional_enum" +# ] +# +# for field in expected_fields: +# assert field in params, f"Missing field '{field}' in serialized parameters" +# print(f" ✓ Field '{field}' present: {params[field]}") +# +# print("\n✓ All parameter fields serialized correctly\n") +# return True +# +# except Exception as e: +# print(f"✗ FAILED: {e}") +# import traceback +# traceback.print_exc() +# return False +# +# +# def test_graph_with_enum_parameters(): +# """Test that graph can handle nodes with enum parameters""" +# print("=" * 80) +# print("TEST 2: Graph with Enum Parameters") +# print("=" * 80) +# +# try: +# # Create and register node in library +# library = QualibrationLibrary.get_active_library(create=True) +# node = QualibrationNode("enum_test_node", parameters=ComprehensiveParameters()) +# library.nodes.add(node) +# print("✓ Node added to library") +# +# # Create graph with the node +# with QualibrationGraph.build("test_graph") as graph: +# graph.add_node(library.nodes.get_nocopy("enum_test_node").copy(name="node1")) +# graph.add_node(library.nodes.get_nocopy("enum_test_node").copy(name="node2")) +# graph.connect("node1", "node2") +# +# print("✓ Graph built successfully") +# +# # Test that full_parameters_class was built correctly +# full_params_class = graph.full_parameters_class +# print(f"✓ Full parameters class created: {full_params_class.__name__}") +# +# # Verify the structure +# assert hasattr(full_params_class, 'model_fields'), "Missing model_fields" +# assert 'parameters' in full_params_class.model_fields, "Missing 'parameters' field" +# assert 'nodes' in full_params_class.model_fields, "Missing 'nodes' field" +# print("✓ Full parameters class has correct structure") +# +# # Test serialization of the graph +# serialized = graph.serialize() +# print("✓ Graph serialized successfully") +# +# # Verify nodes parameters are in serialized output +# assert "nodes" in serialized, "Missing 'nodes' in serialized output" +# nodes_data = serialized["nodes"] +# assert "node1" in nodes_data, "Missing 'node1' in nodes" +# assert "node2" in nodes_data, "Missing 'node2' in nodes" +# print(f" ✓ Node1 parameters: {list(nodes_data['node1'].keys())}") +# print(f" ✓ Node2 parameters: {list(nodes_data['node2'].keys())}") +# +# # Verify enum fields are present in node parameters +# node1_params = nodes_data["node1"] +# if "parameters" in node1_params: +# node1_params = node1_params["parameters"] +# +# assert "color_enum" in node1_params, "Missing 'color_enum' in node1 parameters" +# assert "priority_enum" in node1_params, "Missing 'priority_enum' in node1 parameters" +# print( +# f" ✓ Enum fields present in node1: color={node1_params['color_enum']}, priority={node1_params['priority_enum']}") +# +# print("\n✓ Graph with enum parameters works correctly\n") +# return True +# +# except Exception as e: +# print(f"✗ FAILED: {e}") +# import traceback +# traceback.print_exc() +# return False +# +# +# def test_parameter_class_serialize(): +# """Test the serialize() class method directly""" +# print("=" * 80) +# print("TEST 3: Direct Parameter Serialization") +# print("=" * 80) +# +# try: +# # Test the serialize method +# serialized = ComprehensiveParameters.serialize() +# print("✓ Parameters class serialized successfully") +# print(f" Serialized structure: {list(serialized.keys())}") +# +# # Verify all fields are present +# expected_fields = [ +# "qubits", "str_value", "int_value", "float_value", "bool_value", +# "none_value", "list_value", "dict_value", "color_enum", +# "priority_enum", "optional_enum" +# ] +# +# for field in expected_fields: +# assert field in serialized, f"Missing field '{field}' in serialized class" +# field_info = serialized[field] +# print(f" ✓ Field '{field}': {field_info}") +# +# print("\n✓ Direct serialization works correctly\n") +# return True +# +# except Exception as e: +# print(f"✗ FAILED: {e}") +# import traceback +# traceback.print_exc() +# return False +# +# +# def test_enum_schema_structure(): +# """Test that enum schema is correctly structured after serialization""" +# print("=" * 80) +# print("TEST 4: Enum Schema Structure Validation") +# print("=" * 80) +# +# try: +# serialized = ComprehensiveParameters.serialize() +# +# # Check color_enum structure +# color_schema = serialized["color_enum"] +# print(f" Color enum schema: {color_schema}") +# +# # Should have type information +# assert "type" in color_schema or "enum" in color_schema or "allOf" in color_schema, \ +# "Enum schema missing type information" +# +# # Check priority_enum structure +# priority_schema = serialized["priority_enum"] +# print(f" Priority enum schema: {priority_schema}") +# +# assert "type" in priority_schema or "enum" in priority_schema or "allOf" in priority_schema, \ +# "Enum schema missing type information" +# +# print("\n✓ Enum schemas are properly structured\n") +# return True +# +# except Exception as e: +# print(f"✗ FAILED: {e}") +# import traceback +# traceback.print_exc() +# return False +# +# +# if __name__ == "__main__": +# print("\n" + "=" * 80) +# print("RUNNING COMPREHENSIVE PARAMETER SERIALIZATION TESTS") +# print("=" * 80 + "\n") +# +# results = [] +# +# # Run all tests +# results.append(("Node Serialization", test_node_serialization())) +# results.append(("Graph with Enums", test_graph_with_enum_parameters())) +# results.append(("Direct Serialization", test_parameter_class_serialize())) +# results.append(("Enum Schema Structure", test_enum_schema_structure())) +# +# # Summary +# print("\n" + "=" * 80) +# print("TEST SUMMARY") +# print("=" * 80) +# +# for test_name, passed in results: +# status = "✓ PASSED" if passed else "✗ FAILED" +# print(f"{status}: {test_name}") +# +# all_passed = all(result[1] for result in results) +# +# print("=" * 80) +# if all_passed: +# print("✓ ALL TESTS PASSED - Serialization working correctly!") +# else: +# print("✗ SOME TESTS FAILED - Review failures above") +# print("=" * 80 + "\n") \ No newline at end of file From 4935e689859ace99c87c39e45d255b2630d51592 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Tue, 23 Dec 2025 17:06:12 +0200 Subject: [PATCH 32/73] added tests can merge after linting --- .../test_execution_parameters.py | 11 --- .../test_node_and_graph_parameters.py | 68 +++++++++++++++++++ 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/tests/unit/test_parameters/test_execution_parameters.py b/tests/unit/test_parameters/test_execution_parameters.py index 75f0dba..85509f5 100644 --- a/tests/unit/test_parameters/test_execution_parameters.py +++ b/tests/unit/test_parameters/test_execution_parameters.py @@ -1,5 +1,4 @@ from email.policy import default -from enum import Enum import pytest from pydantic import Field @@ -13,10 +12,6 @@ ) from qualibrate.utils.type_protocols import TargetType -class Color(Enum): - RED = "Red" - GREEN = "Green" - BLUE = "Blue" class Node1(NodeParameters): qubits: list[TargetType] | None = Field( @@ -31,12 +26,6 @@ class Node2(NodeParameters): ) float_value: float = 2.0 -class Node3(NodeParameters): - qubits: list[TargetType] | None = Field( - default_factory=lambda: ["d", "e", "f"] - ) - color: Color = Color.RED - class Graph(GraphParameters): qubits: list[TargetType] = Field(default_factory=lambda: ["1", "2", "3"]) diff --git a/tests/unit/test_parameters/test_node_and_graph_parameters.py b/tests/unit/test_parameters/test_node_and_graph_parameters.py index d078850..016150b 100644 --- a/tests/unit/test_parameters/test_node_and_graph_parameters.py +++ b/tests/unit/test_parameters/test_node_and_graph_parameters.py @@ -1,18 +1,35 @@ +from enum import Enum + import pytest from pydantic import ValidationError from qualibrate.parameters import GraphParameters, NodeParameters +class Color(Enum): + RED = "Red" + GREEN = "Green" + BLUE = "Blue" + class TestCreateParameters: class SampleNodeParameters(NodeParameters): qubits: list[str] | None = None other_param: str = "test" + class SampleNodeParametersWithEnum(NodeParameters): + qubits: list[str] | None = None + other_param: str = "test" + color : Color = Color.RED + class SampleGraphParameters(GraphParameters): qubits: list[str] | None = None other_param: str = "test" + class SampleGraphParametersWithEnum(GraphParameters): + qubits: list[str] | None = None + other_param: str = "test" + color : Color = Color.RED + @pytest.mark.parametrize( "parameters_class", [SampleNodeParameters, SampleGraphParameters] ) @@ -85,3 +102,54 @@ def test_serialize_no_exclude_param(self, parameters_class): "is_targets": False, }, } + + @pytest.mark.parametrize( + "parameters_class", [SampleNodeParametersWithEnum,SampleGraphParametersWithEnum] + ) + def test_serialize_includes_enum_values(self, parameters_class): + serialized = parameters_class.serialize(exclude_targets=False) + # Ensure 'color' field has enum values + assert "color" in serialized + assert "enum" in serialized["color"] + enum_values = serialized["color"]["enum"] + expected_values = [e.value for e in Color] + assert set(enum_values) == set(expected_values) + assert serialized["color"]["default"] == Color.RED.value + + @pytest.mark.parametrize( + "parameters_class", [SampleNodeParametersWithEnum, SampleGraphParametersWithEnum] + ) + def test_serialize_exclude_targets_enum(self, parameters_class): + serialized = parameters_class.serialize(exclude_targets=True) + # 'color' is not a target field, so should be present + assert "color" in serialized + + @pytest.mark.parametrize( + "parameters_class", [SampleNodeParametersWithEnum, SampleGraphParametersWithEnum] + ) + def test_serialize_include_targets_with_enum(self, parameters_class): + expected = { + "qubits": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "default": None, + "title": "Qubits", + "is_targets": True, + }, + "other_param": { + "default": "test", + "title": "Other Param", + "type": "string", + "is_targets": False, + }, + "color": { + "default": "Red", + "enum": ["Red", "Green", "Blue"], + "title": "Color", + "type": "string", + "is_targets": False, + }, + } + assert parameters_class.serialize(exclude_targets=False) == expected \ No newline at end of file From 2bcc6fd8395801d3211e2c4633aafc3e7905ff85 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Tue, 23 Dec 2025 17:08:18 +0200 Subject: [PATCH 33/73] removed commented content --- .../test_execution_parameters.py | 241 +----------------- 1 file changed, 1 insertion(+), 240 deletions(-) diff --git a/tests/unit/test_parameters/test_execution_parameters.py b/tests/unit/test_parameters/test_execution_parameters.py index 85509f5..3de600a 100644 --- a/tests/unit/test_parameters/test_execution_parameters.py +++ b/tests/unit/test_parameters/test_execution_parameters.py @@ -205,243 +205,4 @@ def test_serialize_none_parameters_class(self, mocker): "qualibrate.parameters.GraphParameters" ), ): - ExecutionParameters.serialize() - -# -# class Color(Enum): -# RED = "red" -# GREEN = "green" -# BLUE = "blue" -# -# -# class Priority(Enum): -# LOW = 1 -# MEDIUM = 2 -# HIGH = 3 -# -# -# class ComprehensiveParameters(NodeParameters): -# """Test all parameter types that should be supported""" -# # Required for NodeParameters -# qubits: list[str] = Field(default_factory=list) -# -# # Simple types (should already work) -# str_value: str = "test" -# int_value: int = 1 -# float_value: float = 1.0 -# bool_value: bool = True -# none_value: Optional[str] = None -# -# # Collections (should already work) -# list_value: list[int] = Field(default_factory=lambda: [1, 2, 3]) -# dict_value: dict[str, int] = Field(default_factory=lambda: {"a": 1}) -# -# # Enums (the new case we're fixing) -# color_enum: Color = Color.RED -# priority_enum: Priority = Priority.MEDIUM -# optional_enum: Optional[Color] = None -# -# -# def test_node_serialization(): -# """Test that node with comprehensive parameters can be created and serialized""" -# print("=" * 80) -# print("TEST 1: Node Creation and Serialization") -# print("=" * 80) -# -# try: -# # Create node with comprehensive parameters -# node = QualibrationNode("test_node", parameters=ComprehensiveParameters()) -# print("✓ Node created successfully") -# -# # Test serialization -# serialized = node.serialize() -# print("✓ Node serialized successfully") -# print(f" Serialized keys: {list(serialized.keys())}") -# -# # Check parameters are in serialized output -# assert "parameters" in serialized, "Missing 'parameters' in serialized output" -# params = serialized["parameters"] -# print(f" Parameter keys: {list(params.keys())}") -# -# # Verify all fields are present -# expected_fields = [ -# "qubits", "str_value", "int_value", "float_value", "bool_value", -# "none_value", "list_value", "dict_value", "color_enum", -# "priority_enum", "optional_enum" -# ] -# -# for field in expected_fields: -# assert field in params, f"Missing field '{field}' in serialized parameters" -# print(f" ✓ Field '{field}' present: {params[field]}") -# -# print("\n✓ All parameter fields serialized correctly\n") -# return True -# -# except Exception as e: -# print(f"✗ FAILED: {e}") -# import traceback -# traceback.print_exc() -# return False -# -# -# def test_graph_with_enum_parameters(): -# """Test that graph can handle nodes with enum parameters""" -# print("=" * 80) -# print("TEST 2: Graph with Enum Parameters") -# print("=" * 80) -# -# try: -# # Create and register node in library -# library = QualibrationLibrary.get_active_library(create=True) -# node = QualibrationNode("enum_test_node", parameters=ComprehensiveParameters()) -# library.nodes.add(node) -# print("✓ Node added to library") -# -# # Create graph with the node -# with QualibrationGraph.build("test_graph") as graph: -# graph.add_node(library.nodes.get_nocopy("enum_test_node").copy(name="node1")) -# graph.add_node(library.nodes.get_nocopy("enum_test_node").copy(name="node2")) -# graph.connect("node1", "node2") -# -# print("✓ Graph built successfully") -# -# # Test that full_parameters_class was built correctly -# full_params_class = graph.full_parameters_class -# print(f"✓ Full parameters class created: {full_params_class.__name__}") -# -# # Verify the structure -# assert hasattr(full_params_class, 'model_fields'), "Missing model_fields" -# assert 'parameters' in full_params_class.model_fields, "Missing 'parameters' field" -# assert 'nodes' in full_params_class.model_fields, "Missing 'nodes' field" -# print("✓ Full parameters class has correct structure") -# -# # Test serialization of the graph -# serialized = graph.serialize() -# print("✓ Graph serialized successfully") -# -# # Verify nodes parameters are in serialized output -# assert "nodes" in serialized, "Missing 'nodes' in serialized output" -# nodes_data = serialized["nodes"] -# assert "node1" in nodes_data, "Missing 'node1' in nodes" -# assert "node2" in nodes_data, "Missing 'node2' in nodes" -# print(f" ✓ Node1 parameters: {list(nodes_data['node1'].keys())}") -# print(f" ✓ Node2 parameters: {list(nodes_data['node2'].keys())}") -# -# # Verify enum fields are present in node parameters -# node1_params = nodes_data["node1"] -# if "parameters" in node1_params: -# node1_params = node1_params["parameters"] -# -# assert "color_enum" in node1_params, "Missing 'color_enum' in node1 parameters" -# assert "priority_enum" in node1_params, "Missing 'priority_enum' in node1 parameters" -# print( -# f" ✓ Enum fields present in node1: color={node1_params['color_enum']}, priority={node1_params['priority_enum']}") -# -# print("\n✓ Graph with enum parameters works correctly\n") -# return True -# -# except Exception as e: -# print(f"✗ FAILED: {e}") -# import traceback -# traceback.print_exc() -# return False -# -# -# def test_parameter_class_serialize(): -# """Test the serialize() class method directly""" -# print("=" * 80) -# print("TEST 3: Direct Parameter Serialization") -# print("=" * 80) -# -# try: -# # Test the serialize method -# serialized = ComprehensiveParameters.serialize() -# print("✓ Parameters class serialized successfully") -# print(f" Serialized structure: {list(serialized.keys())}") -# -# # Verify all fields are present -# expected_fields = [ -# "qubits", "str_value", "int_value", "float_value", "bool_value", -# "none_value", "list_value", "dict_value", "color_enum", -# "priority_enum", "optional_enum" -# ] -# -# for field in expected_fields: -# assert field in serialized, f"Missing field '{field}' in serialized class" -# field_info = serialized[field] -# print(f" ✓ Field '{field}': {field_info}") -# -# print("\n✓ Direct serialization works correctly\n") -# return True -# -# except Exception as e: -# print(f"✗ FAILED: {e}") -# import traceback -# traceback.print_exc() -# return False -# -# -# def test_enum_schema_structure(): -# """Test that enum schema is correctly structured after serialization""" -# print("=" * 80) -# print("TEST 4: Enum Schema Structure Validation") -# print("=" * 80) -# -# try: -# serialized = ComprehensiveParameters.serialize() -# -# # Check color_enum structure -# color_schema = serialized["color_enum"] -# print(f" Color enum schema: {color_schema}") -# -# # Should have type information -# assert "type" in color_schema or "enum" in color_schema or "allOf" in color_schema, \ -# "Enum schema missing type information" -# -# # Check priority_enum structure -# priority_schema = serialized["priority_enum"] -# print(f" Priority enum schema: {priority_schema}") -# -# assert "type" in priority_schema or "enum" in priority_schema or "allOf" in priority_schema, \ -# "Enum schema missing type information" -# -# print("\n✓ Enum schemas are properly structured\n") -# return True -# -# except Exception as e: -# print(f"✗ FAILED: {e}") -# import traceback -# traceback.print_exc() -# return False -# -# -# if __name__ == "__main__": -# print("\n" + "=" * 80) -# print("RUNNING COMPREHENSIVE PARAMETER SERIALIZATION TESTS") -# print("=" * 80 + "\n") -# -# results = [] -# -# # Run all tests -# results.append(("Node Serialization", test_node_serialization())) -# results.append(("Graph with Enums", test_graph_with_enum_parameters())) -# results.append(("Direct Serialization", test_parameter_class_serialize())) -# results.append(("Enum Schema Structure", test_enum_schema_structure())) -# -# # Summary -# print("\n" + "=" * 80) -# print("TEST SUMMARY") -# print("=" * 80) -# -# for test_name, passed in results: -# status = "✓ PASSED" if passed else "✗ FAILED" -# print(f"{status}: {test_name}") -# -# all_passed = all(result[1] for result in results) -# -# print("=" * 80) -# if all_passed: -# print("✓ ALL TESTS PASSED - Serialization working correctly!") -# else: -# print("✗ SOME TESTS FAILED - Review failures above") -# print("=" * 80 + "\n") \ No newline at end of file + ExecutionParameters.serialize() \ No newline at end of file From 96f13d12e44fca2f5a00f5e78861825192804efb Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Tue, 23 Dec 2025 17:09:42 +0200 Subject: [PATCH 34/73] linted ready to merge --- qualibrate/utils/parameters.py | 3 +-- .../demo_node.py | 7 +++---- .../test_parameters/test_execution_parameters.py | 4 +--- .../test_node_and_graph_parameters.py | 16 ++++++++++------ 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/qualibrate/utils/parameters.py b/qualibrate/utils/parameters.py index d856314..fb54719 100644 --- a/qualibrate/utils/parameters.py +++ b/qualibrate/utils/parameters.py @@ -35,7 +35,6 @@ def recursive_properties_solver( ) else: properties[name] = resolved - if 'default' in structure: + if "default" in structure: properties[name]["default"] = structure["default"] return properties - diff --git a/tests/calibration_nodes_and_graphs_for_testing/demo_node.py b/tests/calibration_nodes_and_graphs_for_testing/demo_node.py index f4544e2..3797ef3 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/demo_node.py +++ b/tests/calibration_nodes_and_graphs_for_testing/demo_node.py @@ -1,11 +1,8 @@ -from typing import Optional +from enum import Enum from pydantic import Field from qualibrate import NodeParameters, QualibrationNode -from enum import Enum - -from qualibrate.models.outcome import Outcome class Color(Enum): @@ -13,6 +10,7 @@ class Color(Enum): GREEN = "green" BLUE = "blue" + class Parameters(NodeParameters): qubits: list[str] = Field(default_factory=list) str_value: str = "test" @@ -20,6 +18,7 @@ class Parameters(NodeParameters): float_value: float = 1 random_color: Color = Color.BLUE + node = QualibrationNode("test_node", parameters=Parameters()) diff --git a/tests/unit/test_parameters/test_execution_parameters.py b/tests/unit/test_parameters/test_execution_parameters.py index 3de600a..e1be2c2 100644 --- a/tests/unit/test_parameters/test_execution_parameters.py +++ b/tests/unit/test_parameters/test_execution_parameters.py @@ -1,5 +1,3 @@ -from email.policy import default - import pytest from pydantic import Field @@ -205,4 +203,4 @@ def test_serialize_none_parameters_class(self, mocker): "qualibrate.parameters.GraphParameters" ), ): - ExecutionParameters.serialize() \ No newline at end of file + ExecutionParameters.serialize() diff --git a/tests/unit/test_parameters/test_node_and_graph_parameters.py b/tests/unit/test_parameters/test_node_and_graph_parameters.py index 016150b..bb49ad3 100644 --- a/tests/unit/test_parameters/test_node_and_graph_parameters.py +++ b/tests/unit/test_parameters/test_node_and_graph_parameters.py @@ -5,6 +5,7 @@ from qualibrate.parameters import GraphParameters, NodeParameters + class Color(Enum): RED = "Red" GREEN = "Green" @@ -19,7 +20,7 @@ class SampleNodeParameters(NodeParameters): class SampleNodeParametersWithEnum(NodeParameters): qubits: list[str] | None = None other_param: str = "test" - color : Color = Color.RED + color: Color = Color.RED class SampleGraphParameters(GraphParameters): qubits: list[str] | None = None @@ -28,7 +29,7 @@ class SampleGraphParameters(GraphParameters): class SampleGraphParametersWithEnum(GraphParameters): qubits: list[str] | None = None other_param: str = "test" - color : Color = Color.RED + color: Color = Color.RED @pytest.mark.parametrize( "parameters_class", [SampleNodeParameters, SampleGraphParameters] @@ -104,7 +105,8 @@ def test_serialize_no_exclude_param(self, parameters_class): } @pytest.mark.parametrize( - "parameters_class", [SampleNodeParametersWithEnum,SampleGraphParametersWithEnum] + "parameters_class", + [SampleNodeParametersWithEnum, SampleGraphParametersWithEnum], ) def test_serialize_includes_enum_values(self, parameters_class): serialized = parameters_class.serialize(exclude_targets=False) @@ -117,7 +119,8 @@ def test_serialize_includes_enum_values(self, parameters_class): assert serialized["color"]["default"] == Color.RED.value @pytest.mark.parametrize( - "parameters_class", [SampleNodeParametersWithEnum, SampleGraphParametersWithEnum] + "parameters_class", + [SampleNodeParametersWithEnum, SampleGraphParametersWithEnum], ) def test_serialize_exclude_targets_enum(self, parameters_class): serialized = parameters_class.serialize(exclude_targets=True) @@ -125,7 +128,8 @@ def test_serialize_exclude_targets_enum(self, parameters_class): assert "color" in serialized @pytest.mark.parametrize( - "parameters_class", [SampleNodeParametersWithEnum, SampleGraphParametersWithEnum] + "parameters_class", + [SampleNodeParametersWithEnum, SampleGraphParametersWithEnum], ) def test_serialize_include_targets_with_enum(self, parameters_class): expected = { @@ -152,4 +156,4 @@ def test_serialize_include_targets_with_enum(self, parameters_class): "is_targets": False, }, } - assert parameters_class.serialize(exclude_targets=False) == expected \ No newline at end of file + assert parameters_class.serialize(exclude_targets=False) == expected From 27bad1dd914c0affb534f5a093cecfb3e952860f Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Tue, 23 Dec 2025 18:51:34 +0200 Subject: [PATCH 35/73] fixed orchestrator target results bug, reformat code and linting and can merge --- qualibrate/orchestration/basic_orchestrator.py | 3 ++- .../demo_node_that_fails_targets.py | 2 -- .../demo_node_that_succeeds_targets.py | 1 - ...st_node.py => graph_to_test_orchestrator_target_results.py} | 1 - 4 files changed, 2 insertions(+), 5 deletions(-) rename tests/unit/test_qualibration_graph/calibrations/{one_more_test_node.py => graph_to_test_orchestrator_target_results.py} (97%) diff --git a/qualibrate/orchestration/basic_orchestrator.py b/qualibrate/orchestration/basic_orchestrator.py index c21b9a0..84b98cb 100644 --- a/qualibrate/orchestration/basic_orchestrator.py +++ b/qualibrate/orchestration/basic_orchestrator.py @@ -555,7 +555,8 @@ def _fill_final_outcomes(self) -> None: map( lambda outcome: outcome == Outcome.SUCCESSFUL, [ - node.outcomes.get(target, Outcome.FAILED) + # node.outcomes.get(target, Outcome.FAILED) + node.outcomes.get(target,Outcome.SUCCESSFUL) for node in elements_without_successors ], ) diff --git a/tests/calibration_nodes_and_graphs_for_testing/demo_node_that_fails_targets.py b/tests/calibration_nodes_and_graphs_for_testing/demo_node_that_fails_targets.py index 0da1436..db9b6e9 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/demo_node_that_fails_targets.py +++ b/tests/calibration_nodes_and_graphs_for_testing/demo_node_that_fails_targets.py @@ -10,7 +10,6 @@ class Parameters(NodeParameters): node = QualibrationNode("demo_node_that_fails_targets", parameters=Parameters()) -# node.parameters = Parameters(qubits=["q1","q2","q3","q4"]) @node.run_action() @@ -18,5 +17,4 @@ def node_runs_indication(node: QualibrationNode): node.log("node is running") for target in node.parameters.targets: node.outcomes[target] = Outcome.FAILED - node.outcomes["q1"] = Outcome.SUCCESSFUL return node.outcomes diff --git a/tests/calibration_nodes_and_graphs_for_testing/demo_node_that_succeeds_targets.py b/tests/calibration_nodes_and_graphs_for_testing/demo_node_that_succeeds_targets.py index 0988284..ccfd73b 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/demo_node_that_succeeds_targets.py +++ b/tests/calibration_nodes_and_graphs_for_testing/demo_node_that_succeeds_targets.py @@ -12,7 +12,6 @@ class Parameters(NodeParameters): node = QualibrationNode( "demo_node_that_succeeds_targets", parameters=Parameters() ) -# node.parameters = Parameters(qubits=["q1","q2","q3","q4"]) @node.run_action() diff --git a/tests/unit/test_qualibration_graph/calibrations/one_more_test_node.py b/tests/unit/test_qualibration_graph/calibrations/graph_to_test_orchestrator_target_results.py similarity index 97% rename from tests/unit/test_qualibration_graph/calibrations/one_more_test_node.py rename to tests/unit/test_qualibration_graph/calibrations/graph_to_test_orchestrator_target_results.py index d4df66b..edcf127 100644 --- a/tests/unit/test_qualibration_graph/calibrations/one_more_test_node.py +++ b/tests/unit/test_qualibration_graph/calibrations/graph_to_test_orchestrator_target_results.py @@ -29,7 +29,6 @@ class Parameters(GraphParameters): with QualibrationGraph.build( "graph_with_operational_condition_and_loop", parameters=Parameters(qubits=[f"q{i}" for i in range(1,5)]), - # orchestrator=BasicOrchestrator(skip_failed=True), ) as graph: node = library.nodes.get_nocopy(USED_NODE).copy(name="node") node2 = library.nodes.get_nocopy("demo_node_that_fails_targets").copy(name="node2") From 7de127f1bfaf5a694c3a2030f4d761c8d74d6c95 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Wed, 24 Dec 2025 10:51:15 +0200 Subject: [PATCH 36/73] fixed orchestrator bug case, seem that all tests are good, need to make sure we test skip failed targets scenario which does seem covered --- qualibrate/orchestration/basic_orchestrator.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/qualibrate/orchestration/basic_orchestrator.py b/qualibrate/orchestration/basic_orchestrator.py index 84b98cb..48e93eb 100644 --- a/qualibrate/orchestration/basic_orchestrator.py +++ b/qualibrate/orchestration/basic_orchestrator.py @@ -550,12 +550,17 @@ def _fill_final_outcomes(self) -> None: elements_without_successors = list( filter(lambda n: len(successors[n]) == 0, successors.keys()) ) - for target in self.initial_targets or []: + final_targets = { + target + for element in elements_without_successors + for target in element.outcomes + } + skipped_targets = set(self.initial_targets) - final_targets + for target in final_targets or []: successful = all( map( lambda outcome: outcome == Outcome.SUCCESSFUL, [ - # node.outcomes.get(target, Outcome.FAILED) node.outcomes.get(target,Outcome.SUCCESSFUL) for node in elements_without_successors ], @@ -564,6 +569,8 @@ def _fill_final_outcomes(self) -> None: self.final_outcomes[target] = ( Outcome.SUCCESSFUL if successful else Outcome.FAILED ) + for skipped_target in skipped_targets: + self.final_outcomes[skipped_target] = Outcome.FAILED def _current_and_predecessors_statuses( From eac42ea57ffa20d9140effe54f908ec680d46d61 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Wed, 24 Dec 2025 11:25:09 +0200 Subject: [PATCH 37/73] added tests for new orchestrator logic --- .../test_basic_orchestrator.py | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/tests/unit/test_orchestration/test_basic_orchestrator.py b/tests/unit/test_orchestration/test_basic_orchestrator.py index bdcfc73..aeb6eec 100644 --- a/tests/unit/test_orchestration/test_basic_orchestrator.py +++ b/tests/unit/test_orchestration/test_basic_orchestrator.py @@ -1376,3 +1376,87 @@ def edges_getitem(key): # Success edge should be empty assert success_edge_data[QualibrationGraph.EDGE_TARGETS_FIELD] == [] + + +class TestFillFinalOutcomes: + def setup_method(self): + # Common setup for all tests + self.orchestrator = BasicOrchestrator() + self.orchestrator.final_outcomes = {} + self.orchestrator.initial_targets = ["t1", "t2", "t3", "t4"] + + def make_node(self, outcomes): + node = MagicMock() + node.outcomes = outcomes # dict: {target: Outcome} + return node + + def test_all_targets_successful(self, mocker): + """All targets reach leaves and succeed.""" + leaf1 = self.make_node({"t1": Outcome.SUCCESSFUL, "t2": Outcome.SUCCESSFUL}) + leaf2 = self.make_node({"t1": Outcome.SUCCESSFUL, "t2": Outcome.SUCCESSFUL}) + + # Patch the nx_graph property + mock_nx_graph = mocker.patch.object( + self.orchestrator.__class__, + "nx_graph", + new_callable=PropertyMock + ) + + mock_graph = MagicMock() + mock_graph.succ = {leaf1: [], leaf2: []} + mock_nx_graph.return_value = mock_graph + + self.orchestrator._fill_final_outcomes() + + assert self.orchestrator.final_outcomes["t1"] == Outcome.SUCCESSFUL + assert self.orchestrator.final_outcomes["t2"] == Outcome.SUCCESSFUL + # t3 and t4 never reached any leaf → fail + assert self.orchestrator.final_outcomes["t3"] == Outcome.FAILED + assert self.orchestrator.final_outcomes["t4"] == Outcome.FAILED + + def test_some_targets_fail(self, mocker): + """Some targets fail on at least one leaf node.""" + leaf1 = self.make_node({"t1": Outcome.SUCCESSFUL, "t2": Outcome.FAILED}) + leaf2 = self.make_node({"t1": Outcome.SUCCESSFUL, "t2": Outcome.SUCCESSFUL}) + + # Patch the nx_graph property + mock_nx_graph = mocker.patch.object( + self.orchestrator.__class__, + "nx_graph", + new_callable=PropertyMock + ) + + mock_graph = MagicMock() + mock_graph.succ = {leaf1: [], leaf2: []} + mock_nx_graph.return_value = mock_graph + + self.orchestrator._fill_final_outcomes() + + # t1 succeeds on all → successful + assert self.orchestrator.final_outcomes["t1"] == Outcome.SUCCESSFUL + # t2 fails on one leaf → failed + assert self.orchestrator.final_outcomes["t2"] == Outcome.FAILED + # t3 and t4 never reached any leaf → fail + assert self.orchestrator.final_outcomes["t3"] == Outcome.FAILED + assert self.orchestrator.final_outcomes["t4"] == Outcome.FAILED + + def test_target_missing_from_leaf_outcomes(self, mocker): + """Target missing from leaf outcomes defaults to SUCCESSFUL.""" + leaf1 = self.make_node({"t1": Outcome.SUCCESSFUL}) + + mock_nx_graph = mocker.patch.object( + self.orchestrator.__class__, + "nx_graph", + new_callable=PropertyMock + ) + + mock_graph = MagicMock() + mock_graph.succ = {leaf1: []} + mock_nx_graph.return_value = mock_graph + + self.orchestrator.initial_targets = ["t1", "t2"] + self.orchestrator._fill_final_outcomes() + + assert self.orchestrator.final_outcomes["t1"] == Outcome.SUCCESSFUL + # t2 never appears in any leaf node outcomes + assert self.orchestrator.final_outcomes["t2"] == Outcome.FAILED \ No newline at end of file From 0f4af6e8de30c1b7acbf012c65870881e7669648 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Wed, 24 Dec 2025 15:34:29 +0200 Subject: [PATCH 38/73] linted some, still linting missing, need to finish linting --- .../orchestration/basic_orchestrator.py | 4 +-- qualibrate/qualibration_graph.py | 17 +++++++----- .../test_basic_orchestrator.py | 26 +++++++++---------- .../test_graph_serialize.py | 11 ++++---- 4 files changed, 31 insertions(+), 27 deletions(-) diff --git a/qualibrate/orchestration/basic_orchestrator.py b/qualibrate/orchestration/basic_orchestrator.py index 48e93eb..dec3c1e 100644 --- a/qualibrate/orchestration/basic_orchestrator.py +++ b/qualibrate/orchestration/basic_orchestrator.py @@ -555,13 +555,13 @@ def _fill_final_outcomes(self) -> None: for element in elements_without_successors for target in element.outcomes } - skipped_targets = set(self.initial_targets) - final_targets + skipped_targets = set(self.initial_targets) - final_targets for target in final_targets or []: successful = all( map( lambda outcome: outcome == Outcome.SUCCESSFUL, [ - node.outcomes.get(target,Outcome.SUCCESSFUL) + node.outcomes.get(target, Outcome.SUCCESSFUL) for node in elements_without_successors ], ) diff --git a/qualibrate/qualibration_graph.py b/qualibrate/qualibration_graph.py index 353dd58..370780f 100644 --- a/qualibrate/qualibration_graph.py +++ b/qualibrate/qualibration_graph.py @@ -876,7 +876,6 @@ def serialize_graph_representation(self) -> Mapping[str, Any]: operational_condition_data: dict[str, bool | str] = {} - if condition_name and condition_content: operational_condition_data.update( { @@ -890,7 +889,7 @@ def serialize_graph_representation(self) -> Mapping[str, Any]: QualibrationGraph.RUN_SCENARIO_FIELD, Outcome.SUCCESSFUL, ) - == Outcome.SUCCESSFUL, + == Outcome.SUCCESSFUL, } if operational_condition_data: data["condition"] = operational_condition_data @@ -900,7 +899,7 @@ def serialize_graph_representation(self) -> Mapping[str, Any]: "id": f"{node_name}->{target_name}", "source": node_name, "target": target_name, - "data": data + "data": data, } ) @@ -915,9 +914,13 @@ def _add_loop_to_edge(self, node_name: str) -> dict[str, Any]: if node_name not in self._loop_conditions: raise KeyError(f"Loop condition not found for node {node_name}") - node_conditions: LoopCondition[GraphElementTypeVar] = self._loop_conditions[node_name] - label,content = self._get_operational_condition_signature_and_content(node_conditions) - data = {'loop':{}} + node_conditions: LoopCondition[GraphElementTypeVar] = ( + self._loop_conditions[node_name] + ) + label, content = self._get_operational_condition_signature_and_content( + node_conditions + ) + data = {"loop": {}} if label and content: data["loop"]["label"] = label data["loop"]["content"] = content @@ -927,7 +930,7 @@ def _add_loop_to_edge(self, node_name: str) -> dict[str, Any]: "id": f"{node_name}->{node_name}", "source": node_name, "target": node_name, - "data": data, + "data": data, } def __serialize_data(self, /, **kwargs: Any) -> Mapping[str, Any]: diff --git a/tests/unit/test_orchestration/test_basic_orchestrator.py b/tests/unit/test_orchestration/test_basic_orchestrator.py index aeb6eec..6e8b4c3 100644 --- a/tests/unit/test_orchestration/test_basic_orchestrator.py +++ b/tests/unit/test_orchestration/test_basic_orchestrator.py @@ -1392,14 +1392,16 @@ def make_node(self, outcomes): def test_all_targets_successful(self, mocker): """All targets reach leaves and succeed.""" - leaf1 = self.make_node({"t1": Outcome.SUCCESSFUL, "t2": Outcome.SUCCESSFUL}) - leaf2 = self.make_node({"t1": Outcome.SUCCESSFUL, "t2": Outcome.SUCCESSFUL}) + leaf1 = self.make_node( + {"t1": Outcome.SUCCESSFUL, "t2": Outcome.SUCCESSFUL} + ) + leaf2 = self.make_node( + {"t1": Outcome.SUCCESSFUL, "t2": Outcome.SUCCESSFUL} + ) # Patch the nx_graph property mock_nx_graph = mocker.patch.object( - self.orchestrator.__class__, - "nx_graph", - new_callable=PropertyMock + self.orchestrator.__class__, "nx_graph", new_callable=PropertyMock ) mock_graph = MagicMock() @@ -1417,13 +1419,13 @@ def test_all_targets_successful(self, mocker): def test_some_targets_fail(self, mocker): """Some targets fail on at least one leaf node.""" leaf1 = self.make_node({"t1": Outcome.SUCCESSFUL, "t2": Outcome.FAILED}) - leaf2 = self.make_node({"t1": Outcome.SUCCESSFUL, "t2": Outcome.SUCCESSFUL}) + leaf2 = self.make_node( + {"t1": Outcome.SUCCESSFUL, "t2": Outcome.SUCCESSFUL} + ) # Patch the nx_graph property mock_nx_graph = mocker.patch.object( - self.orchestrator.__class__, - "nx_graph", - new_callable=PropertyMock + self.orchestrator.__class__, "nx_graph", new_callable=PropertyMock ) mock_graph = MagicMock() @@ -1445,9 +1447,7 @@ def test_target_missing_from_leaf_outcomes(self, mocker): leaf1 = self.make_node({"t1": Outcome.SUCCESSFUL}) mock_nx_graph = mocker.patch.object( - self.orchestrator.__class__, - "nx_graph", - new_callable=PropertyMock + self.orchestrator.__class__, "nx_graph", new_callable=PropertyMock ) mock_graph = MagicMock() @@ -1459,4 +1459,4 @@ def test_target_missing_from_leaf_outcomes(self, mocker): assert self.orchestrator.final_outcomes["t1"] == Outcome.SUCCESSFUL # t2 never appears in any leaf node outcomes - assert self.orchestrator.final_outcomes["t2"] == Outcome.FAILED \ No newline at end of file + assert self.orchestrator.final_outcomes["t2"] == Outcome.FAILED diff --git a/tests/unit/test_qualibration_graph/test_graph_serialize.py b/tests/unit/test_qualibration_graph/test_graph_serialize.py index 6141bc9..0bd3075 100644 --- a/tests/unit/test_qualibration_graph/test_graph_serialize.py +++ b/tests/unit/test_qualibration_graph/test_graph_serialize.py @@ -175,6 +175,7 @@ def test_serialize_with_nested_graphs( ], } + def test_serialize_with_nested_graphs_and_connect_on_failure( qualibration_lib: QualibrationLibrary, graph_params: GraphParameters ): @@ -295,9 +296,9 @@ def test_serialize_graph_with_operational_condition_and_loop( if e["source"] == node_name and e["target"] == node3_name ) assert node3_edge["data"]["connect_on"] is False # Failure path - assert "label" in node3_edge["data"]['condition'] + assert "label" in node3_edge["data"]["condition"] assert ( - node3_edge["data"]['condition']["label"] == "" + node3_edge["data"]["condition"]["label"] == "" ) # Lambda function @@ -325,6 +326,6 @@ def test_serialize_graph_with_multiple_operational_conditions( # Both failure edges should have operational conditions for edge in failure_edges: # assert edge["data"]["operational_condition"] is True - assert "label" in edge["data"]['condition'] - assert "description" in edge["data"]['condition'] - assert edge["data"]['condition']["description"] is not None + assert "label" in edge["data"]["condition"] + assert "description" in edge["data"]["condition"] + assert edge["data"]["condition"]["description"] is not None From 13fd14a6fd1cd23df33d248a08d12a736d161593 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Wed, 24 Dec 2025 20:07:03 +0200 Subject: [PATCH 39/73] linted the code, theres a bug where it complains about a line and after a fix it reverts it so its inmpossible to fix, added ignore for e501 so it doesnt block us --- pyproject.toml | 1 + .../orchestration/basic_orchestrator.py | 8 +++-- qualibrate/qualibration_graph.py | 12 ++++--- .../test_basic_orchestrator.py | 3 +- .../test_basic_orchestrator.py | 35 +++++++++++-------- .../test_qualibration_graph/test_graph.py | 20 ----------- .../test_graph_serialize.py | 6 ++-- 7 files changed, 39 insertions(+), 46 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0ff8119..04409df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ poethepoet = "^0.37.0" line-length = 80 target-version = "py310" exclude = ["calibrations"] +ignore = ["E501"] [tool.ruff.lint] select = [ diff --git a/qualibrate/orchestration/basic_orchestrator.py b/qualibrate/orchestration/basic_orchestrator.py index dec3c1e..4d81818 100644 --- a/qualibrate/orchestration/basic_orchestrator.py +++ b/qualibrate/orchestration/basic_orchestrator.py @@ -216,7 +216,8 @@ def _set_out_targets_for_element( f"Can't set out targets of {element} without run summary" ) - # self.nx_graph.edges[element, successor]["operational_condition"] is of type OperationalCondition + # self.nx_graph.edges[element, successor]["operational_condition"] + # is of type OperationalCondition has_on_failed_successors = any( self.nx_graph.edges[element, successor][ QualibrationGraph.RUN_SCENARIO_FIELD @@ -265,7 +266,8 @@ def _execute_condition( ) -> list[TargetType]: if operational_condition.on_generator is not None: executed_condition = operational_condition.on_generator() - # priming the generator, we need to get to the point where the generator expects out two variables + # priming the generator, we need to get to the point + # where the generator expects out two variables executed_condition.send(None) return [ target @@ -555,7 +557,7 @@ def _fill_final_outcomes(self) -> None: for element in elements_without_successors for target in element.outcomes } - skipped_targets = set(self.initial_targets) - final_targets + skipped_targets = set(self.initial_targets or []) - final_targets for target in final_targets or []: successful = all( map( diff --git a/qualibrate/qualibration_graph.py b/qualibrate/qualibration_graph.py index 370780f..86f3913 100644 --- a/qualibrate/qualibration_graph.py +++ b/qualibrate/qualibration_graph.py @@ -142,7 +142,6 @@ def __init__( ) self._elements = dict(nodes) self._connectivity = { - # connectivity: Outcome.SUCCESSFUL connectivity: { QualibrationGraph.RUN_SCENARIO_FIELD: Outcome.SUCCESSFUL, QualibrationGraph.OPERATIONAL_CONDITION_FIELD: OperationalCondition[ @@ -863,8 +862,12 @@ def serialize_graph_representation(self) -> Mapping[str, Any]: ) for adj in adjacency: - # where QualibrationGraph.OPERATIONAL_CONDITION_FIELD is used, it's because it is how we save the operational condition in the graph - # where it's not used but operational_condition key is still used, it's because this is the key returned to the ui and not necessarily the key we save the data in + # where QualibrationGraph.OPERATIONAL_CONDITION_FIELD is used, + # it's because it is how + # we save the operational condition in the graph + # where it's not used but operational_condition key + # is still used, it's because this is the key returned + # to the ui and not necessarily the key we save the data in # the same as for a scenario target_name = adj["id"] @@ -920,7 +923,7 @@ def _add_loop_to_edge(self, node_name: str) -> dict[str, Any]: label, content = self._get_operational_condition_signature_and_content( node_conditions ) - data = {"loop": {}} + data: dict[str, Any] = {"loop": {}} if label and content: data["loop"]["label"] = label data["loop"]["content"] = content @@ -1175,7 +1178,6 @@ def _connect( f"Both '{s}' and '{d}' must be added before connecting." ) edge = (s, d) - # bug fix it if edge in self._connectivity: raise ValueError(f"Edge '{edge}' already exists.") self._connectivity[edge] = { diff --git a/tests/integration/test_orchestration/test_basic_orchestrator.py b/tests/integration/test_orchestration/test_basic_orchestrator.py index 573ec46..b435e98 100644 --- a/tests/integration/test_orchestration/test_basic_orchestrator.py +++ b/tests/integration/test_orchestration/test_basic_orchestrator.py @@ -182,7 +182,8 @@ def test_traverse_graph_with_conditional_failed_edge_filters_targets(): Expected: - node1 runs with all targets (set by orchestrator) - Successful targets go to node2_success (set by orchestrator) - - Failed targets are filtered by condition before going to node3_retry (set by orchestrator) + - Failed targets are filtered by condition + before going to node3_retry (set by orchestrator) """ orchestrator = BasicOrchestrator(skip_failed=False) diff --git a/tests/unit/test_orchestration/test_basic_orchestrator.py b/tests/unit/test_orchestration/test_basic_orchestrator.py index 6e8b4c3..772799a 100644 --- a/tests/unit/test_orchestration/test_basic_orchestrator.py +++ b/tests/unit/test_orchestration/test_basic_orchestrator.py @@ -954,7 +954,8 @@ def condition_gen(): def test_set_out_targets_with_operational_condition_on_failure(mocker): - """Test _set_out_targets routes targets through operational condition on failed edge""" + """Test _set_out_targets routes targets through + operational condition on failed edge""" orchestrator = BasicOrchestrator() mock_node = MagicMock() @@ -1018,7 +1019,8 @@ def edges_getitem(key): "q2", ] - # Failed edge should get failed targets filtered by condition (q3 has error_count=2 <= 3) + # Failed edge should get failed targets filtered by condition + # (q3 has error_count=2 <= 3) assert failure_edge_data[QualibrationGraph.EDGE_TARGETS_FIELD] == ["q3"] @@ -1056,7 +1058,8 @@ def test_connect_on_failure_with_condition_lambda_function( def test_connect_on_failure_with_condition_generator( pre_setup_graph_init, mock_library ): - """Test that connect_on_failure() with generator condition works correctly""" + """Test that connect_on_failure() with + generator condition works correctly""" (nodes, _, _, _) = pre_setup_graph_init def condition_gen(): @@ -1088,7 +1091,8 @@ def condition_gen(): def test_get_next_element_skips_already_finished_node(mocker): - """Test that get_next_element skips nodes that are already finished (duplicate queue entries)""" + """Test that get_next_element skips nodes + that are already finished (duplicate queue entries)""" orchestrator = BasicOrchestrator() mock_finished_node = MagicMock() @@ -1123,7 +1127,8 @@ def check_finished(node): def test_get_next_element_skips_node_with_unfinished_predecessors(mocker): - """Test that get_next_element skips node if any predecessor is not finished""" + """Test that get_next_element skips + node if any predecessor is not finished""" orchestrator = BasicOrchestrator() mock_node = MagicMock() @@ -1140,11 +1145,10 @@ def test_get_next_element_skips_node_with_unfinished_predecessors(mocker): ) mock_nx_graph.return_value.pred = {mock_node: [mock_pred1, mock_pred2]} - # Mock check_node_finished: pred1 finished, pred2 not finished, node not finished + # Mock check_node_finished: pred1 finished, + # pred2 not finished, node not finished def check_finished(node): - if node == mock_pred1: - return True - return False + return node == mock_pred1 mocker.patch.object( orchestrator, "check_node_finished", side_effect=check_finished @@ -1159,7 +1163,8 @@ def check_finished(node): def test_execute_condition_with_empty_condition_returns_all_targets(): - """Test _execute_condition with empty operational condition (no function, no generator) returns all targets""" + """Test _execute_condition with empty operational condition + (no function, no generator) returns all targets""" orchestrator = BasicOrchestrator() @@ -1176,7 +1181,8 @@ def test_execute_condition_with_empty_condition_returns_all_targets(): def test_set_out_targets_with_empty_operational_condition_on_failure(mocker): - """Test that empty operational condition on failed edge passes all failed targets""" + """Test that empty operational condition + on failed edge passes all failed targets""" orchestrator = BasicOrchestrator() mock_node = MagicMock() @@ -1236,7 +1242,8 @@ def edges_getitem(key): def test_get_next_element_returns_node_when_all_predecessors_finished(mocker): - """Test that get_next_element returns node when all predecessors are finished""" + """Test that get_next_element returns + node when all predecessors are finished""" orchestrator = BasicOrchestrator() mock_node = MagicMock() @@ -1252,9 +1259,7 @@ def test_get_next_element_returns_node_when_all_predecessors_finished(mocker): mock_nx_graph.return_value.pred = {mock_node: [mock_pred1, mock_pred2]} def check_finished(node): - if node == mock_node: - return False # The node itself is not finished - return True + return node == mock_node mocker.patch.object( orchestrator, "check_node_finished", side_effect=check_finished diff --git a/tests/unit/test_qualibration_graph/test_graph.py b/tests/unit/test_qualibration_graph/test_graph.py index 2e82239..7284789 100644 --- a/tests/unit/test_qualibration_graph/test_graph.py +++ b/tests/unit/test_qualibration_graph/test_graph.py @@ -670,23 +670,3 @@ def test__validate_graph_acyclic_without_cycle( orchestrator=mock_orchestrator, ) graph._validate_graph_acyclic() - - def test_connect_then_connect_on_failure_same_edge_overwrites( - self, pre_setup_graph_init, mock_library - ): - """Test that calling connect() then connect_on_failure() - on the same edge overwrites to FAILED scenario""" - (nodes, _, _, _) = pre_setup_graph_init - - with QualibrationGraph.build( - name="test_graph", - parameters=GraphParameters(), - ) as graph: - graph.add_nodes(nodes["node1"], nodes["node2"]) - graph.connect("node1", "node2") # First: SUCCESS - with pytest.raises(ValueError): - graph.connect_on_failure("node1", "node2") # Then: FAILED - - # # Should only have one edge with FAILED outcome (last write wins) - # assert len(graph._connectivity) == 1 - # assert graph._connectivity[("node1", "node2")][QualibrationGraph.RUN_SCENARIO_FIELD] == Outcome.FAILED diff --git a/tests/unit/test_qualibration_graph/test_graph_serialize.py b/tests/unit/test_qualibration_graph/test_graph_serialize.py index 0bd3075..35b9851 100644 --- a/tests/unit/test_qualibration_graph/test_graph_serialize.py +++ b/tests/unit/test_qualibration_graph/test_graph_serialize.py @@ -253,7 +253,8 @@ def test_serialize_graph_with_operational_condition_and_loop( loop_edge = next((e for e in edges if e["source"] == e["target"]), None) assert loop_edge is not None assert loop_edge["id"] == f"{node_name}->{node_name}" - # Check for loop-specific fields (adjust based on actual _add_loop_to_edge implementation) + # Check for loop-specific fields + # (adjust based on actual _add_loop_to_edge implementation) assert "data" in loop_edge # The loop edge should have max_iterations field assert "loop" in loop_edge["data"] @@ -305,7 +306,8 @@ def test_serialize_graph_with_operational_condition_and_loop( def test_serialize_graph_with_multiple_operational_conditions( qualibration_lib: QualibrationLibrary, graph_params: GraphParameters ): - """Test that both failure edges with different conditions are properly serialized.""" + """Test that both failure edges with different + conditions are properly serialized.""" g = qualibration_lib.graphs["graph_with_operational_condition_and_loop"] serialized = g.serialize_graph_representation() From 2891aac817b031f5f96e941c1d9b935b5bacfe0e Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Wed, 24 Dec 2025 20:28:09 +0200 Subject: [PATCH 40/73] removed commented code and fixed test --- tests/unit/test_orchestration/test_basic_orchestrator.py | 2 +- .../graph_to_test_orchestrator_target_results.py | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/tests/unit/test_orchestration/test_basic_orchestrator.py b/tests/unit/test_orchestration/test_basic_orchestrator.py index 772799a..d62803b 100644 --- a/tests/unit/test_orchestration/test_basic_orchestrator.py +++ b/tests/unit/test_orchestration/test_basic_orchestrator.py @@ -1259,7 +1259,7 @@ def test_get_next_element_returns_node_when_all_predecessors_finished(mocker): mock_nx_graph.return_value.pred = {mock_node: [mock_pred1, mock_pred2]} def check_finished(node): - return node == mock_node + return node != mock_node mocker.patch.object( orchestrator, "check_node_finished", side_effect=check_finished diff --git a/tests/unit/test_qualibration_graph/calibrations/graph_to_test_orchestrator_target_results.py b/tests/unit/test_qualibration_graph/calibrations/graph_to_test_orchestrator_target_results.py index edcf127..fb17c14 100644 --- a/tests/unit/test_qualibration_graph/calibrations/graph_to_test_orchestrator_target_results.py +++ b/tests/unit/test_qualibration_graph/calibrations/graph_to_test_orchestrator_target_results.py @@ -16,7 +16,6 @@ from typing import ClassVar from qualibrate import QualibrationGraph, QualibrationLibrary, QualibrationNode, GraphParameters -from qualibrate.models.outcome import Outcome class Parameters(GraphParameters): targets_name: ClassVar[str] = "qubits" @@ -37,29 +36,21 @@ class Parameters(GraphParameters): graph.add_nodes(node, node2, node3, node4) - # Add loop with fidelity check (retry if fidelity < 0.95) - # Access results from namespace graph.loop( "node", on=lambda node, target: node.namespace.get("calibration_results").get(target).get("fidelity") < 0.7, max_iterations=3 ) - # Connect failure paths with conditions - # node2: gets retriable errors (error_type == "retriable") - - graph.connect_on_failure( "node", "node2", on=lambda node, target: node.namespace.get("calibration_results").get(target).get("error_type") == "retriable" ) - # node3: gets permanent errors (error_type == "permanent") graph.connect_on_failure( "node", "node3", - # on=lambda node, target: node.namespace.get("calibration_results").get(target).get("error_type") == "permanent" on= lambda node, target: True ) From 9d69e89b5f24395dbb625f42e1bb206951d5352b Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Thu, 25 Dec 2025 19:09:54 +0200 Subject: [PATCH 41/73] qubits work, need to make sure change in node doesnt break anything and add tests --- qualibrate/qualibration_node.py | 35 ++++++++++++++++++- .../demo_node.py | 13 +++++-- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/qualibrate/qualibration_node.py b/qualibrate/qualibration_node.py index 493423a..6002c19 100644 --- a/qualibrate/qualibration_node.py +++ b/qualibrate/qualibration_node.py @@ -125,9 +125,11 @@ def __init__( *, parameters_class: type[ParametersType] | None = None, modes: RunModes | None = None, + machine : NodeMachineType | None = None, ): if self.__class__.active_node is not None: return + self.machine = machine name = name or self.__class__._get_name_from_stack_frame() logger.info(f"Creating node {name}") parameters = self.__class__._validate_passed_parameters_options( @@ -142,7 +144,7 @@ def __init__( # class is used just for passing reference to the running instance self._fraction_complete = 0.0 self.results: dict[Any, Any] = {} - self.machine: NodeMachineType | None = None + # self.machine: NodeMachineType | None = None self.storage_manager: StorageManager[Self] | None = None # Initialize the ActionManager to handle run_action logic. @@ -518,6 +520,37 @@ def load_from_id( build_params_class=isinstance(caller, type), ) + + def serialize(self, **kwargs: Any) -> Mapping[str, Any]: + # Get base QRunnable serialization + data = dict(super().serialize(**kwargs)) + + if self.machine is not None and hasattr(self.machine, "active_qubits") and hasattr(self.machine, "qubits"): + qubits = self.machine.qubits.keys() + active_qubits = {qubit.name for qubit in self.machine.active_qubits} + + # Build metadata for each qubit + metadata = {} + for qubit in qubits: + qubit_info = self.machine.qubits[qubit] + gate_fidelity = None + if hasattr(qubit_info, "gate_fidelity") and hasattr(qubit_info.gate_fidelity, "averaged"): + gate_fidelity = qubit_info.gate_fidelity.averaged + metadata[qubit] = { + 'active': qubit in active_qubits, + 'fidelity': gate_fidelity, + } + + # Store metadata in the serialized data + if 'parameters' not in data: + data['parameters'] = {} + if 'qubits' not in data['parameters']: + data['parameters']['qubits'] = {} + + data['parameters']['qubits']['metadata'] = metadata + + return data + def _post_run( self, initial_targets: Sequence[TargetType], diff --git a/tests/calibration_nodes_and_graphs_for_testing/demo_node.py b/tests/calibration_nodes_and_graphs_for_testing/demo_node.py index b46f473..02586d8 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/demo_node.py +++ b/tests/calibration_nodes_and_graphs_for_testing/demo_node.py @@ -1,5 +1,5 @@ from pydantic import Field - +# from quam.components import BasicQuam from qualibrate import NodeParameters, QualibrationNode @@ -11,9 +11,18 @@ class Parameters(NodeParameters): node = QualibrationNode("test_node", parameters=Parameters()) +# node = QualibrationNode("test_node", parameters=Parameters(),machine = BasicQuam().load()) node.parameters = Parameters() +# node.machine = BasicQuam.load() @node.run_action() def node_runs_indication(node: QualibrationNode): - node.log("node is running") + # node.log("node is running") + # node.log("connect machine") + # node.machine = BasicQuam.load() + # node.log("connected machine") + # node.log(node.machine) + data = node.serialize() + print(data) + return data From e1ef2f30304ddce5c9de975ef757c56b22529a4d Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Sun, 28 Dec 2025 11:22:04 +0200 Subject: [PATCH 42/73] added testing for new qubits serialization --- .../demo_node.py | 6 +- .../test_qubit_inside_node_serialization.py | 243 ++++++++++++++++++ 2 files changed, 246 insertions(+), 3 deletions(-) create mode 100644 tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py diff --git a/tests/calibration_nodes_and_graphs_for_testing/demo_node.py b/tests/calibration_nodes_and_graphs_for_testing/demo_node.py index 02586d8..4899320 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/demo_node.py +++ b/tests/calibration_nodes_and_graphs_for_testing/demo_node.py @@ -1,5 +1,5 @@ from pydantic import Field -# from quam.components import BasicQuam +from quam.components import BasicQuam from qualibrate import NodeParameters, QualibrationNode @@ -11,9 +11,9 @@ class Parameters(NodeParameters): node = QualibrationNode("test_node", parameters=Parameters()) -# node = QualibrationNode("test_node", parameters=Parameters(),machine = BasicQuam().load()) +node = QualibrationNode("test_node", parameters=Parameters(),machine = BasicQuam().load()) node.parameters = Parameters() -# node.machine = BasicQuam.load() +node.machine = BasicQuam.load() @node.run_action() diff --git a/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py b/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py new file mode 100644 index 0000000..7c3ad5c --- /dev/null +++ b/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py @@ -0,0 +1,243 @@ +import pytest +from unittest.mock import Mock, MagicMock +from typing import Any +from qualibrate import QualibrationNode +from qualibrate.parameters import NodeParameters + +class MockQubit: + """Mock qubit class for testing""" + + def __init__(self, name: str, gate_fidelity_value: float | None = None): + self.name = name + if gate_fidelity_value is not None: + self.gate_fidelity = Mock() + self.gate_fidelity.averaged = gate_fidelity_value + else: + self.gate_fidelity = Mock(spec=[]) # No 'averaged' attribute + + +class MockMachine: + """Mock machine class for testing""" + + def __init__(self, qubits_dict: dict[str, Any], active_qubits_list: list[MockQubit]): + self.qubits = qubits_dict + self.active_qubits = active_qubits_list + + +class TestNodeSerialization: + """Test suite for QualibrationNode.serialize() method""" + + @pytest.fixture + def base_node(self): + """Create a basic node without machine for testing""" + node = QualibrationNode( + name="test_node", + parameters=NodeParameters() + ) + return node + + @pytest.fixture + def mock_qubits(self): + """Create mock qubits with various configurations""" + qubits = { + 'qA1': MockQubit('qA1', 0.9607522653458166), + 'qA2': MockQubit('qA2', 0.9789885462390904), + 'qA3': MockQubit('qA3', 0.9987151301821622), + 'qA4': MockQubit('qA4', None), # No fidelity + 'qA5': MockQubit('qA5', 1.0000000657302262), + } + return qubits + + @pytest.fixture + def node_with_machine(self, base_node, mock_qubits): + """Create a node with a machine that has qubits""" + active_qubits = [ + mock_qubits['qA1'], + mock_qubits['qA2'], + mock_qubits['qA3'], + ] + + machine = MockMachine(mock_qubits, active_qubits) + base_node.machine = machine + return base_node + + def test_serialize_without_machine(self, base_node): + """Test serialization when machine is None""" + result = base_node.serialize() + + assert isinstance(result, dict) + assert 'name' in result + assert result['name'] == 'test_node' + # Should not have qubits metadata when machine is None + if 'parameters' in result and 'qubits' in result['parameters']: + assert 'metadata' not in result['parameters']['qubits'] + + def test_serialize_with_machine_no_attributes(self, base_node): + """Test serialization when machine lacks required attributes""" + base_node.machine = Mock(spec=[]) # Machine without active_qubits or qubits + + result = base_node.serialize() + + assert isinstance(result, dict) + # Should not have qubits metadata when machine lacks attributes + if 'parameters' in result and 'qubits' in result['parameters']: + assert 'metadata' not in result['parameters']['qubits'] + + def test_serialize_with_complete_machine(self, node_with_machine, mock_qubits): + """Test serialization with a complete machine setup""" + result = node_with_machine.serialize() + + assert isinstance(result, dict) + assert 'parameters' in result + assert 'qubits' in result['parameters'] + assert 'metadata' in result['parameters']['qubits'] + + metadata = result['parameters']['qubits']['metadata'] + + # Check all qubits are in metadata + assert set(metadata.keys()) == set(mock_qubits.keys()) + + # Check active qubits + assert metadata['qA1']['active'] is True + assert metadata['qA2']['active'] is True + assert metadata['qA3']['active'] is True + assert metadata['qA4']['active'] is False + assert metadata['qA5']['active'] is False + + # Check fidelity values + assert metadata['qA1']['fidelity'] == 0.9607522653458166 + assert metadata['qA2']['fidelity'] == 0.9789885462390904 + assert metadata['qA3']['fidelity'] == 0.9987151301821622 + assert metadata['qA4']['fidelity'] is None + assert metadata['qA5']['fidelity'] == 1.0000000657302262 + + def test_serialize_active_qubit_identification(self, base_node, mock_qubits): + """Test that active qubits are correctly identified""" + active_qubits = [mock_qubits['qA1'], mock_qubits['qA3']] + machine = MockMachine(mock_qubits, active_qubits) + base_node.machine = machine + + result = base_node.serialize() + metadata = result['parameters']['qubits']['metadata'] + + # Only qA1 and qA3 should be active + assert metadata['qA1']['active'] is True + assert metadata['qA2']['active'] is False + assert metadata['qA3']['active'] is True + assert metadata['qA4']['active'] is False + assert metadata['qA5']['active'] is False + + def test_serialize_qubit_without_gate_fidelity(self, base_node): + """Test serialization when qubit doesn't have gate_fidelity attribute""" + qubits = { + 'qA1': Mock(spec=['name']), # No gate_fidelity attribute + } + qubits['qA1'].name = 'qA1' + + machine = MockMachine(qubits, []) + base_node.machine = machine + + result = base_node.serialize() + metadata = result['parameters']['qubits']['metadata'] + + assert metadata['qA1']['active'] is False + assert metadata['qA1']['fidelity'] is None + + def test_serialize_qubit_without_averaged_fidelity(self, base_node): + """Test serialization when gate_fidelity doesn't have 'averaged' attribute""" + qubits = { + 'qA1': Mock(), + } + qubits['qA1'].name = 'qA1' + qubits['qA1'].gate_fidelity = Mock(spec=[]) # No 'averaged' attribute + + machine = MockMachine(qubits, []) + base_node.machine = machine + + result = base_node.serialize() + metadata = result['parameters']['qubits']['metadata'] + + assert metadata['qA1']['fidelity'] is None + + def test_serialize_empty_qubits(self, base_node): + """Test serialization with empty qubits dictionary""" + machine = MockMachine({}, []) + base_node.machine = machine + + result = base_node.serialize() + + assert 'parameters' in result + assert 'qubits' in result['parameters'] + assert 'metadata' in result['parameters']['qubits'] + assert result['parameters']['qubits']['metadata'] == {} + + def test_serialize_preserves_base_data(self, node_with_machine): + """Test that serialize preserves all base class serialization data""" + result = node_with_machine.serialize() + + # Should have base fields from QRunnable + assert 'name' in result + assert result['name'] == 'test_node' + + # Should have parameters field + assert 'parameters' in result + + def test_serialize_metadata_structure(self, node_with_machine): + """Test the structure of metadata for each qubit""" + result = node_with_machine.serialize() + metadata = result['parameters']['qubits']['metadata'] + + for qubit_name, qubit_data in metadata.items(): + # Each qubit should have exactly these keys + assert set(qubit_data.keys()) == {'active', 'fidelity'} + # active should be boolean + assert isinstance(qubit_data['active'], bool) + # fidelity should be float or None + assert qubit_data['fidelity'] is None or isinstance(qubit_data['fidelity'], float) + + +class TestNodeSerializationEdgeCases: + """Test edge cases and error conditions""" + + def test_serialize_with_partial_machine_attributes(self): + """Test machine with only some attributes""" + node = QualibrationNode(name="test", parameters=NodeParameters()) + + # Machine with only active_qubits + node.machine = Mock(spec=['active_qubits']) + node.machine.active_qubits = [] + + result = node.serialize() + assert isinstance(result, dict) + + # Machine with only qubits + node.machine = Mock(spec=['qubits']) + node.machine.qubits = Mock() + node.machine.qubits.keys = Mock(return_value=[]) + + result = node.serialize() + assert isinstance(result, dict) + + def test_serialize_large_number_of_qubits(self): + """Test serialization with a large number of qubits""" + node = QualibrationNode(name="test", parameters=NodeParameters()) + + # Create 100 qubits + qubits = { + f'q{i}': MockQubit(f'q{i}', 0.95 + i * 0.0001) + for i in range(100) + } + + active_qubits = [qubits[f'q{i}'] for i in range(0, 100, 2)] # Every other qubit + machine = MockMachine(qubits, active_qubits) + node.machine = machine + + result = node.serialize() + metadata = result['parameters']['qubits']['metadata'] + + assert len(metadata) == 100 + # Check some active/inactive states + assert metadata['q0']['active'] is True + assert metadata['q1']['active'] is False + assert metadata['q50']['active'] is True + assert metadata['q51']['active'] is False \ No newline at end of file From 24a345c8856c96e6b5c66e8350015f4da5cff513 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Sun, 28 Dec 2025 11:36:27 +0200 Subject: [PATCH 43/73] linted the code --- qualibrate/qualibration_node.py | 30 ++-- .../demo_node.py | 14 +- .../test_qubit_inside_node_serialization.py | 170 ++++++++++-------- 3 files changed, 110 insertions(+), 104 deletions(-) diff --git a/qualibrate/qualibration_node.py b/qualibrate/qualibration_node.py index 6002c19..4bb8b0c 100644 --- a/qualibrate/qualibration_node.py +++ b/qualibrate/qualibration_node.py @@ -125,11 +125,10 @@ def __init__( *, parameters_class: type[ParametersType] | None = None, modes: RunModes | None = None, - machine : NodeMachineType | None = None, + machine: NodeMachineType | None = None, ): if self.__class__.active_node is not None: return - self.machine = machine name = name or self.__class__._get_name_from_stack_frame() logger.info(f"Creating node {name}") parameters = self.__class__._validate_passed_parameters_options( @@ -144,8 +143,8 @@ def __init__( # class is used just for passing reference to the running instance self._fraction_complete = 0.0 self.results: dict[Any, Any] = {} - # self.machine: NodeMachineType | None = None self.storage_manager: StorageManager[Self] | None = None + self.machine = machine # Initialize the ActionManager to handle run_action logic. self._action_manager = ActionManager() @@ -520,12 +519,15 @@ def load_from_id( build_params_class=isinstance(caller, type), ) - def serialize(self, **kwargs: Any) -> Mapping[str, Any]: # Get base QRunnable serialization data = dict(super().serialize(**kwargs)) - if self.machine is not None and hasattr(self.machine, "active_qubits") and hasattr(self.machine, "qubits"): + if ( + self.machine is not None + and hasattr(self.machine, "active_qubits") + and hasattr(self.machine, "qubits") + ): qubits = self.machine.qubits.keys() active_qubits = {qubit.name for qubit in self.machine.active_qubits} @@ -534,20 +536,22 @@ def serialize(self, **kwargs: Any) -> Mapping[str, Any]: for qubit in qubits: qubit_info = self.machine.qubits[qubit] gate_fidelity = None - if hasattr(qubit_info, "gate_fidelity") and hasattr(qubit_info.gate_fidelity, "averaged"): + if hasattr(qubit_info, "gate_fidelity") and hasattr( + qubit_info.gate_fidelity, "averaged" + ): gate_fidelity = qubit_info.gate_fidelity.averaged metadata[qubit] = { - 'active': qubit in active_qubits, - 'fidelity': gate_fidelity, + "active": qubit in active_qubits, + "fidelity": gate_fidelity, } # Store metadata in the serialized data - if 'parameters' not in data: - data['parameters'] = {} - if 'qubits' not in data['parameters']: - data['parameters']['qubits'] = {} + if "parameters" not in data: + data["parameters"] = {} + if "qubits" not in data["parameters"]: + data["parameters"]["qubits"] = {} - data['parameters']['qubits']['metadata'] = metadata + data["parameters"]["qubits"]["metadata"] = metadata return data diff --git a/tests/calibration_nodes_and_graphs_for_testing/demo_node.py b/tests/calibration_nodes_and_graphs_for_testing/demo_node.py index 4899320..b9e7de9 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/demo_node.py +++ b/tests/calibration_nodes_and_graphs_for_testing/demo_node.py @@ -1,5 +1,5 @@ from pydantic import Field -from quam.components import BasicQuam + from qualibrate import NodeParameters, QualibrationNode @@ -11,18 +11,8 @@ class Parameters(NodeParameters): node = QualibrationNode("test_node", parameters=Parameters()) -node = QualibrationNode("test_node", parameters=Parameters(),machine = BasicQuam().load()) -node.parameters = Parameters() -node.machine = BasicQuam.load() @node.run_action() def node_runs_indication(node: QualibrationNode): - # node.log("node is running") - # node.log("connect machine") - # node.machine = BasicQuam.load() - # node.log("connected machine") - # node.log(node.machine) - data = node.serialize() - print(data) - return data + node.log("node is running") diff --git a/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py b/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py index 7c3ad5c..3e3b259 100644 --- a/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py +++ b/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py @@ -1,9 +1,12 @@ -import pytest -from unittest.mock import Mock, MagicMock from typing import Any +from unittest.mock import Mock + +import pytest + from qualibrate import QualibrationNode from qualibrate.parameters import NodeParameters + class MockQubit: """Mock qubit class for testing""" @@ -19,7 +22,9 @@ def __init__(self, name: str, gate_fidelity_value: float | None = None): class MockMachine: """Mock machine class for testing""" - def __init__(self, qubits_dict: dict[str, Any], active_qubits_list: list[MockQubit]): + def __init__( + self, qubits_dict: dict[str, Any], active_qubits_list: list[MockQubit] + ): self.qubits = qubits_dict self.active_qubits = active_qubits_list @@ -30,21 +35,18 @@ class TestNodeSerialization: @pytest.fixture def base_node(self): """Create a basic node without machine for testing""" - node = QualibrationNode( - name="test_node", - parameters=NodeParameters() - ) + node = QualibrationNode(name="test_node", parameters=NodeParameters()) return node @pytest.fixture def mock_qubits(self): """Create mock qubits with various configurations""" qubits = { - 'qA1': MockQubit('qA1', 0.9607522653458166), - 'qA2': MockQubit('qA2', 0.9789885462390904), - 'qA3': MockQubit('qA3', 0.9987151301821622), - 'qA4': MockQubit('qA4', None), # No fidelity - 'qA5': MockQubit('qA5', 1.0000000657302262), + "qA1": MockQubit("qA1", 0.9607522653458166), + "qA2": MockQubit("qA2", 0.9789885462390904), + "qA3": MockQubit("qA3", 0.9987151301821622), + "qA4": MockQubit("qA4", None), # No fidelity + "qA5": MockQubit("qA5", 1.0000000657302262), } return qubits @@ -52,9 +54,9 @@ def mock_qubits(self): def node_with_machine(self, base_node, mock_qubits): """Create a node with a machine that has qubits""" active_qubits = [ - mock_qubits['qA1'], - mock_qubits['qA2'], - mock_qubits['qA3'], + mock_qubits["qA1"], + mock_qubits["qA2"], + mock_qubits["qA3"], ] machine = MockMachine(mock_qubits, active_qubits) @@ -66,98 +68,105 @@ def test_serialize_without_machine(self, base_node): result = base_node.serialize() assert isinstance(result, dict) - assert 'name' in result - assert result['name'] == 'test_node' + assert "name" in result + assert result["name"] == "test_node" # Should not have qubits metadata when machine is None - if 'parameters' in result and 'qubits' in result['parameters']: - assert 'metadata' not in result['parameters']['qubits'] + if "parameters" in result and "qubits" in result["parameters"]: + assert "metadata" not in result["parameters"]["qubits"] def test_serialize_with_machine_no_attributes(self, base_node): """Test serialization when machine lacks required attributes""" - base_node.machine = Mock(spec=[]) # Machine without active_qubits or qubits + base_node.machine = Mock( + spec=[] + ) # Machine without active_qubits or qubits result = base_node.serialize() assert isinstance(result, dict) # Should not have qubits metadata when machine lacks attributes - if 'parameters' in result and 'qubits' in result['parameters']: - assert 'metadata' not in result['parameters']['qubits'] + if "parameters" in result and "qubits" in result["parameters"]: + assert "metadata" not in result["parameters"]["qubits"] - def test_serialize_with_complete_machine(self, node_with_machine, mock_qubits): + def test_serialize_with_complete_machine( + self, node_with_machine, mock_qubits + ): """Test serialization with a complete machine setup""" result = node_with_machine.serialize() assert isinstance(result, dict) - assert 'parameters' in result - assert 'qubits' in result['parameters'] - assert 'metadata' in result['parameters']['qubits'] + assert "parameters" in result + assert "qubits" in result["parameters"] + assert "metadata" in result["parameters"]["qubits"] - metadata = result['parameters']['qubits']['metadata'] + metadata = result["parameters"]["qubits"]["metadata"] # Check all qubits are in metadata assert set(metadata.keys()) == set(mock_qubits.keys()) # Check active qubits - assert metadata['qA1']['active'] is True - assert metadata['qA2']['active'] is True - assert metadata['qA3']['active'] is True - assert metadata['qA4']['active'] is False - assert metadata['qA5']['active'] is False + assert metadata["qA1"]["active"] is True + assert metadata["qA2"]["active"] is True + assert metadata["qA3"]["active"] is True + assert metadata["qA4"]["active"] is False + assert metadata["qA5"]["active"] is False # Check fidelity values - assert metadata['qA1']['fidelity'] == 0.9607522653458166 - assert metadata['qA2']['fidelity'] == 0.9789885462390904 - assert metadata['qA3']['fidelity'] == 0.9987151301821622 - assert metadata['qA4']['fidelity'] is None - assert metadata['qA5']['fidelity'] == 1.0000000657302262 - - def test_serialize_active_qubit_identification(self, base_node, mock_qubits): + assert metadata["qA1"]["fidelity"] == 0.9607522653458166 + assert metadata["qA2"]["fidelity"] == 0.9789885462390904 + assert metadata["qA3"]["fidelity"] == 0.9987151301821622 + assert metadata["qA4"]["fidelity"] is None + assert metadata["qA5"]["fidelity"] == 1.0000000657302262 + + def test_serialize_active_qubit_identification( + self, base_node, mock_qubits + ): """Test that active qubits are correctly identified""" - active_qubits = [mock_qubits['qA1'], mock_qubits['qA3']] + active_qubits = [mock_qubits["qA1"], mock_qubits["qA3"]] machine = MockMachine(mock_qubits, active_qubits) base_node.machine = machine result = base_node.serialize() - metadata = result['parameters']['qubits']['metadata'] + metadata = result["parameters"]["qubits"]["metadata"] # Only qA1 and qA3 should be active - assert metadata['qA1']['active'] is True - assert metadata['qA2']['active'] is False - assert metadata['qA3']['active'] is True - assert metadata['qA4']['active'] is False - assert metadata['qA5']['active'] is False + assert metadata["qA1"]["active"] is True + assert metadata["qA2"]["active"] is False + assert metadata["qA3"]["active"] is True + assert metadata["qA4"]["active"] is False + assert metadata["qA5"]["active"] is False def test_serialize_qubit_without_gate_fidelity(self, base_node): """Test serialization when qubit doesn't have gate_fidelity attribute""" qubits = { - 'qA1': Mock(spec=['name']), # No gate_fidelity attribute + "qA1": Mock(spec=["name"]), # No gate_fidelity attribute } - qubits['qA1'].name = 'qA1' + qubits["qA1"].name = "qA1" machine = MockMachine(qubits, []) base_node.machine = machine result = base_node.serialize() - metadata = result['parameters']['qubits']['metadata'] + metadata = result["parameters"]["qubits"]["metadata"] - assert metadata['qA1']['active'] is False - assert metadata['qA1']['fidelity'] is None + assert metadata["qA1"]["active"] is False + assert metadata["qA1"]["fidelity"] is None def test_serialize_qubit_without_averaged_fidelity(self, base_node): - """Test serialization when gate_fidelity doesn't have 'averaged' attribute""" + """Test serialization when gate_fidelity doesn't + have 'averaged' attribute""" qubits = { - 'qA1': Mock(), + "qA1": Mock(), } - qubits['qA1'].name = 'qA1' - qubits['qA1'].gate_fidelity = Mock(spec=[]) # No 'averaged' attribute + qubits["qA1"].name = "qA1" + qubits["qA1"].gate_fidelity = Mock(spec=[]) # No 'averaged' attribute machine = MockMachine(qubits, []) base_node.machine = machine result = base_node.serialize() - metadata = result['parameters']['qubits']['metadata'] + metadata = result["parameters"]["qubits"]["metadata"] - assert metadata['qA1']['fidelity'] is None + assert metadata["qA1"]["fidelity"] is None def test_serialize_empty_qubits(self, base_node): """Test serialization with empty qubits dictionary""" @@ -166,34 +175,36 @@ def test_serialize_empty_qubits(self, base_node): result = base_node.serialize() - assert 'parameters' in result - assert 'qubits' in result['parameters'] - assert 'metadata' in result['parameters']['qubits'] - assert result['parameters']['qubits']['metadata'] == {} + assert "parameters" in result + assert "qubits" in result["parameters"] + assert "metadata" in result["parameters"]["qubits"] + assert result["parameters"]["qubits"]["metadata"] == {} def test_serialize_preserves_base_data(self, node_with_machine): """Test that serialize preserves all base class serialization data""" result = node_with_machine.serialize() # Should have base fields from QRunnable - assert 'name' in result - assert result['name'] == 'test_node' + assert "name" in result + assert result["name"] == "test_node" # Should have parameters field - assert 'parameters' in result + assert "parameters" in result def test_serialize_metadata_structure(self, node_with_machine): """Test the structure of metadata for each qubit""" result = node_with_machine.serialize() - metadata = result['parameters']['qubits']['metadata'] + metadata = result["parameters"]["qubits"]["metadata"] - for qubit_name, qubit_data in metadata.items(): + for qubit_data in metadata.values(): # Each qubit should have exactly these keys - assert set(qubit_data.keys()) == {'active', 'fidelity'} + assert set(qubit_data.keys()) == {"active", "fidelity"} # active should be boolean - assert isinstance(qubit_data['active'], bool) + assert isinstance(qubit_data["active"], bool) # fidelity should be float or None - assert qubit_data['fidelity'] is None or isinstance(qubit_data['fidelity'], float) + assert qubit_data["fidelity"] is None or isinstance( + qubit_data["fidelity"], float + ) class TestNodeSerializationEdgeCases: @@ -204,14 +215,14 @@ def test_serialize_with_partial_machine_attributes(self): node = QualibrationNode(name="test", parameters=NodeParameters()) # Machine with only active_qubits - node.machine = Mock(spec=['active_qubits']) + node.machine = Mock(spec=["active_qubits"]) node.machine.active_qubits = [] result = node.serialize() assert isinstance(result, dict) # Machine with only qubits - node.machine = Mock(spec=['qubits']) + node.machine = Mock(spec=["qubits"]) node.machine.qubits = Mock() node.machine.qubits.keys = Mock(return_value=[]) @@ -224,20 +235,21 @@ def test_serialize_large_number_of_qubits(self): # Create 100 qubits qubits = { - f'q{i}': MockQubit(f'q{i}', 0.95 + i * 0.0001) - for i in range(100) + f"q{i}": MockQubit(f"q{i}", 0.95 + i * 0.0001) for i in range(100) } - active_qubits = [qubits[f'q{i}'] for i in range(0, 100, 2)] # Every other qubit + active_qubits = [ + qubits[f"q{i}"] for i in range(0, 100, 2) + ] # Every other qubit machine = MockMachine(qubits, active_qubits) node.machine = machine result = node.serialize() - metadata = result['parameters']['qubits']['metadata'] + metadata = result["parameters"]["qubits"]["metadata"] assert len(metadata) == 100 # Check some active/inactive states - assert metadata['q0']['active'] is True - assert metadata['q1']['active'] is False - assert metadata['q50']['active'] is True - assert metadata['q51']['active'] is False \ No newline at end of file + assert metadata["q0"]["active"] is True + assert metadata["q1"]["active"] is False + assert metadata["q50"]["active"] is True + assert metadata["q51"]["active"] is False From 4b86d073eafc3e1a6884c271cf3f83bb6a624165 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Sun, 28 Dec 2025 11:51:24 +0200 Subject: [PATCH 44/73] removed commented code --- .../demo_node_with_results.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/calibration_nodes_and_graphs_for_testing/demo_node_with_results.py b/tests/calibration_nodes_and_graphs_for_testing/demo_node_with_results.py index d926156..10b022a 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/demo_node_with_results.py +++ b/tests/calibration_nodes_and_graphs_for_testing/demo_node_with_results.py @@ -22,10 +22,6 @@ def simulate_calibration(node: QualibrationNode): if target == "q1": # q1 succeeds results[target] = {"fidelity": 0.98, "error_type": None} - # node.outcomes = { - # "q1": "success", - # "fidelity": 0.98, "error_type": None - # } node.outcomes[target] = Outcome.SUCCESSFUL elif target == "q2": # q2 fails with retriable error From c473c22980c410fad931d83f8c474255965f2a30 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Sun, 28 Dec 2025 11:52:01 +0200 Subject: [PATCH 45/73] removed comments --- .../demo_node_with_results.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/calibration_nodes_and_graphs_for_testing/demo_node_with_results.py b/tests/calibration_nodes_and_graphs_for_testing/demo_node_with_results.py index 10b022a..b0632a0 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/demo_node_with_results.py +++ b/tests/calibration_nodes_and_graphs_for_testing/demo_node_with_results.py @@ -32,7 +32,6 @@ def simulate_calibration(node: QualibrationNode): results[target] = {"fidelity": 0.89, "error_type": "retriable"} node.outcomes[target] = Outcome.FAILED else: # q4 - # q4 fails with permanent error results[target] = {"fidelity": 0.85, "error_type": "retriable"} node.outcomes[target] = Outcome.FAILED # Store results in namespace so they persist From 4d678a2e7733eccfbc76c01cff92ce40602091aa Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Sun, 28 Dec 2025 16:29:12 +0200 Subject: [PATCH 46/73] finished cr --- qualibrate/qualibration_node.py | 63 ++++++++++--------- .../test_qubit_inside_node_serialization.py | 62 +++++++++++------- 2 files changed, 75 insertions(+), 50 deletions(-) diff --git a/qualibrate/qualibration_node.py b/qualibrate/qualibration_node.py index 4bb8b0c..9a9da6d 100644 --- a/qualibrate/qualibration_node.py +++ b/qualibrate/qualibration_node.py @@ -522,36 +522,43 @@ def load_from_id( def serialize(self, **kwargs: Any) -> Mapping[str, Any]: # Get base QRunnable serialization data = dict(super().serialize(**kwargs)) - - if ( - self.machine is not None - and hasattr(self.machine, "active_qubits") - and hasattr(self.machine, "qubits") - ): - qubits = self.machine.qubits.keys() - active_qubits = {qubit.name for qubit in self.machine.active_qubits} - - # Build metadata for each qubit - metadata = {} - for qubit in qubits: - qubit_info = self.machine.qubits[qubit] - gate_fidelity = None - if hasattr(qubit_info, "gate_fidelity") and hasattr( - qubit_info.gate_fidelity, "averaged" - ): - gate_fidelity = qubit_info.gate_fidelity.averaged - metadata[qubit] = { - "active": qubit in active_qubits, - "fidelity": gate_fidelity, + pre_mutated_data = copy.deepcopy(data) + try: + if ( + self.machine is not None + and hasattr(self.machine, "active_qubits") + and hasattr(self.machine, "qubits") + ): + qubits = self.machine.qubits.keys() + active_qubits = { + qubit.name for qubit in self.machine.active_qubits } - # Store metadata in the serialized data - if "parameters" not in data: - data["parameters"] = {} - if "qubits" not in data["parameters"]: - data["parameters"]["qubits"] = {} - - data["parameters"]["qubits"]["metadata"] = metadata + # Build metadata for each qubit + metadata = {} + for qubit in qubits: + qubit_info = self.machine.qubits[qubit] + gate_fidelity = None + if hasattr(qubit_info, "gate_fidelity") and hasattr( + qubit_info.gate_fidelity, "averaged" + ): + gate_fidelity = qubit_info.gate_fidelity.averaged + metadata[qubit] = { + "active": qubit in active_qubits, + "fidelity": gate_fidelity, + } + + # Store metadata in the serialized data + if "parameters" not in data: + data["parameters"] = {} + if "qubits" not in data["parameters"]: + data["parameters"]["qubits"] = {} + + data["parameters"]["qubits"]["metadata"] = metadata + except Exception: + # for any exception that could happen, return the old data as it was + return pre_mutated_data + return data return data diff --git a/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py b/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py index 3e3b259..7119c24 100644 --- a/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py +++ b/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py @@ -29,6 +29,10 @@ def __init__( self.active_qubits = active_qubits_list +def _active_qubits_names(mocked_machine: MockMachine) -> list[str]: + return [active_qubit.name for active_qubit in mocked_machine.active_qubits] + + class TestNodeSerialization: """Test suite for QualibrationNode.serialize() method""" @@ -103,19 +107,34 @@ def test_serialize_with_complete_machine( # Check all qubits are in metadata assert set(metadata.keys()) == set(mock_qubits.keys()) - # Check active qubits - assert metadata["qA1"]["active"] is True - assert metadata["qA2"]["active"] is True - assert metadata["qA3"]["active"] is True - assert metadata["qA4"]["active"] is False - assert metadata["qA5"]["active"] is False + # Check active qubits, this is based on the node_with_machine fixture + active_qubits_names = _active_qubits_names(node_with_machine.machine) + + for qubit in metadata: + assert (metadata[qubit]["active"]) == (qubit in active_qubits_names) # Check fidelity values - assert metadata["qA1"]["fidelity"] == 0.9607522653458166 - assert metadata["qA2"]["fidelity"] == 0.9789885462390904 - assert metadata["qA3"]["fidelity"] == 0.9987151301821622 + assert ( + metadata["qA1"]["fidelity"] + == mock_qubits["qA1"].gate_fidelity.averaged + ) + assert ( + metadata["qA2"]["fidelity"] + == mock_qubits["qA2"].gate_fidelity.averaged + ) + assert ( + metadata["qA3"]["fidelity"] + == mock_qubits["qA3"].gate_fidelity.averaged + ) + # qA4 doesn't have gate_fidelity.averaged, so accessing it should raise + # AttributeError + with pytest.raises(AttributeError): + _ = mock_qubits["qA4"].gate_fidelity.averaged assert metadata["qA4"]["fidelity"] is None - assert metadata["qA5"]["fidelity"] == 1.0000000657302262 + assert ( + metadata["qA5"]["fidelity"] + == mock_qubits["qA5"].gate_fidelity.averaged + ) def test_serialize_active_qubit_identification( self, base_node, mock_qubits @@ -127,13 +146,11 @@ def test_serialize_active_qubit_identification( result = base_node.serialize() metadata = result["parameters"]["qubits"]["metadata"] + active_qubits_names = _active_qubits_names(machine) - # Only qA1 and qA3 should be active - assert metadata["qA1"]["active"] is True - assert metadata["qA2"]["active"] is False - assert metadata["qA3"]["active"] is True - assert metadata["qA4"]["active"] is False - assert metadata["qA5"]["active"] is False + # Only qA1 and qA3 should be activ + for qubit in metadata: + assert (metadata[qubit]["active"]) == (qubit in active_qubits_names) def test_serialize_qubit_without_gate_fidelity(self, base_node): """Test serialization when qubit doesn't have gate_fidelity attribute""" @@ -148,7 +165,7 @@ def test_serialize_qubit_without_gate_fidelity(self, base_node): result = base_node.serialize() metadata = result["parameters"]["qubits"]["metadata"] - assert metadata["qA1"]["active"] is False + assert metadata["qA1"]["active"] not in base_node.machine.active_qubits assert metadata["qA1"]["fidelity"] is None def test_serialize_qubit_without_averaged_fidelity(self, base_node): @@ -247,9 +264,10 @@ def test_serialize_large_number_of_qubits(self): result = node.serialize() metadata = result["parameters"]["qubits"]["metadata"] - assert len(metadata) == 100 + assert len(metadata) == len(qubits) # Check some active/inactive states - assert metadata["q0"]["active"] is True - assert metadata["q1"]["active"] is False - assert metadata["q50"]["active"] is True - assert metadata["q51"]["active"] is False + active_qubits_names = _active_qubits_names(machine) + assert "q0" in active_qubits_names + assert "q1" not in active_qubits_names + assert "q50" in active_qubits_names + assert "q51" not in active_qubits_names From c7d585ecde0b60a0fb5a2b67f643d8ee9399354c Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Sun, 28 Dec 2025 16:50:08 +0200 Subject: [PATCH 47/73] added comments --- qualibrate/utils/parameters.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/qualibrate/utils/parameters.py b/qualibrate/utils/parameters.py index fb54719..555bc57 100644 --- a/qualibrate/utils/parameters.py +++ b/qualibrate/utils/parameters.py @@ -29,11 +29,15 @@ def recursive_properties_solver( structure.update(structure.pop("allOf")[0]) if "$ref" in structure: resolved = resolve_pointer(schema, structure["$ref"][1:]) + # for objects, object json schema has propertieskey + # then they are handled if "properties" in resolved: properties[name] = recursive_properties_solver( resolved["properties"], schema ) else: + # for objects like enums, enums don't have properties key, + # but they have ref so they also need to be handled properties[name] = resolved if "default" in structure: properties[name]["default"] = structure["default"] From bfd53013bc80996fc392a7a044bd6d2effd5fe29 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Sun, 28 Dec 2025 17:18:06 +0200 Subject: [PATCH 48/73] finished cr --- .../test_basic_orchestrator.py | 42 +++---------------- 1 file changed, 6 insertions(+), 36 deletions(-) diff --git a/tests/unit/test_orchestration/test_basic_orchestrator.py b/tests/unit/test_orchestration/test_basic_orchestrator.py index 2af668b..ad3d59a 100644 --- a/tests/unit/test_orchestration/test_basic_orchestrator.py +++ b/tests/unit/test_orchestration/test_basic_orchestrator.py @@ -543,8 +543,9 @@ def test_is_loop_iteration_needed_max_iterations(self): mock_element.run_summary = None mock_graph = MagicMock() + max_iterations = 3 mock_graph._loop_conditions = { - "test_element": LoopCondition(max_iterations=3) + "test_element": LoopCondition(max_iterations=max_iterations) } orchestrator._graph = mock_graph @@ -556,11 +557,9 @@ def test_is_loop_iteration_needed_max_iterations(self): # First yield - initial execution assert next(iteration_generator) is True - # Iterations 1 and 2 should continue - assert iteration_generator.send(None) is True - assert iteration_generator.send(None) is True + for _iteration_index in range(1, max_iterations): + assert iteration_generator.send(None) is True - # Iteration 3 (max reached) should stop assert iteration_generator.send(None) is False def test_is_loop_iteration_needed_on_function_with_reuse_targets( @@ -580,10 +579,11 @@ def test_is_loop_iteration_needed_on_function_with_reuse_targets( def filter_func(element, target): return target in ["q1", "q2"] + max_iterations = 5 mock_graph = MagicMock() mock_graph._loop_conditions = { "test_element": LoopCondition( - on_function=filter_func, max_iterations=5 + on_function=filter_func, max_iterations=max_iterations ) } @@ -682,36 +682,6 @@ def generator_func(): assert iteration_generator.send(None) is True assert iteration_generator.send(None) is False - def test_is_loop_iteration_needed_on_failure(self, mocker): - """Test loop continues when node fails and on_failure is True""" - orchestrator = BasicOrchestrator() - mock_element = MagicMock() - mock_element.name = "test_element" - mock_element.run_summary = None - - mock_graph = MagicMock() - mock_graph._loop_conditions = { - "test_element": LoopCondition(on_failure=True, max_iterations=3) - } - - orchestrator._graph = mock_graph - - iteration_generator = orchestrator._is_loop_iteration_needed( - mock_element - ) - - # Initial execution - assert next(iteration_generator) is True - - # Node failed (error status), should retry - assert iteration_generator.send(None) is True - - # Should continue until max_iterations - assert iteration_generator.send(None) is True - - # Max iterations reached - assert iteration_generator.send(None) is False - def test_traverse_graph_with_loop_condition(self): """Integration test: Graph with a node that loops based on a condition""" From 19f4b788844e93d05a6123e35e9ff35b586ebffa Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Sun, 28 Dec 2025 17:58:45 +0200 Subject: [PATCH 49/73] finished cr comments --- qualibrate/orchestration/basic_orchestrator.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/qualibrate/orchestration/basic_orchestrator.py b/qualibrate/orchestration/basic_orchestrator.py index 4d81818..74503b5 100644 --- a/qualibrate/orchestration/basic_orchestrator.py +++ b/qualibrate/orchestration/basic_orchestrator.py @@ -558,14 +558,15 @@ def _fill_final_outcomes(self) -> None: for target in element.outcomes } skipped_targets = set(self.initial_targets or []) - final_targets - for target in final_targets or []: + for target in final_targets: + outcomes = [ + node.outcomes.get(target, Outcome.SUCCESSFUL) + for node in elements_without_successors + ] successful = all( map( lambda outcome: outcome == Outcome.SUCCESSFUL, - [ - node.outcomes.get(target, Outcome.SUCCESSFUL) - for node in elements_without_successors - ], + outcomes, ) ) self.final_outcomes[target] = ( From bf60b66f48c2919f8de99b19531fdaaf13cde5b6 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Sun, 28 Dec 2025 19:31:49 +0200 Subject: [PATCH 50/73] linted --- qualibrate/orchestration/basic_orchestrator.py | 2 ++ tests/unit/test_orchestration/test_basic_orchestrator.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/qualibrate/orchestration/basic_orchestrator.py b/qualibrate/orchestration/basic_orchestrator.py index 74503b5..5575568 100644 --- a/qualibrate/orchestration/basic_orchestrator.py +++ b/qualibrate/orchestration/basic_orchestrator.py @@ -127,6 +127,8 @@ def get_next_element(self) -> GraphElementTypeVar | None: while not self._execution_queue.empty(): element_to_run = self._execution_queue.get() # Skip if already finished (handles duplicate queue entries) + # for cases like diamond cases where a-> b,c b,c->d + # d will be twice in queue and we dont want to execute it twice if self.check_node_finished(element_to_run): continue diff --git a/tests/unit/test_orchestration/test_basic_orchestrator.py b/tests/unit/test_orchestration/test_basic_orchestrator.py index d62803b..cce0da6 100644 --- a/tests/unit/test_orchestration/test_basic_orchestrator.py +++ b/tests/unit/test_orchestration/test_basic_orchestrator.py @@ -1100,7 +1100,9 @@ def test_get_next_element_skips_already_finished_node(mocker): # Simulate duplicate: add finished node twice, then pending node orchestrator._execution_queue.put(mock_finished_node) - orchestrator._execution_queue.put(mock_finished_node) # Duplicate! + orchestrator._execution_queue.put( + mock_finished_node + ) # Duplicate! (intentionally) orchestrator._execution_queue.put(mock_pending_node) # Mock nx_graph From 27ba843556a690aa102046636667d3e9cb05eb30 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Sun, 28 Dec 2025 19:35:06 +0200 Subject: [PATCH 51/73] added log info --- qualibrate/qualibration_node.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qualibrate/qualibration_node.py b/qualibrate/qualibration_node.py index 9a9da6d..ae85500 100644 --- a/qualibrate/qualibration_node.py +++ b/qualibrate/qualibration_node.py @@ -557,6 +557,8 @@ def serialize(self, **kwargs: Any) -> Mapping[str, Any]: data["parameters"]["qubits"]["metadata"] = metadata except Exception: # for any exception that could happen, return the old data as it was + logger.info("Failed to serialize quam machine, probably used a different quam" + "package hence the machine json load isn't compatible") return pre_mutated_data return data From 7d59992a09e912ba5ea2f3be185784939e0ba0b4 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Sun, 28 Dec 2025 19:35:46 +0200 Subject: [PATCH 52/73] fixed line --- qualibrate/qualibration_node.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/qualibrate/qualibration_node.py b/qualibrate/qualibration_node.py index ae85500..506c47b 100644 --- a/qualibrate/qualibration_node.py +++ b/qualibrate/qualibration_node.py @@ -562,8 +562,6 @@ def serialize(self, **kwargs: Any) -> Mapping[str, Any]: return pre_mutated_data return data - return data - def _post_run( self, initial_targets: Sequence[TargetType], From 75adaa93590f91c3bc0621fb5eb99dd98529cf1b Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Sun, 28 Dec 2025 19:36:04 +0200 Subject: [PATCH 53/73] linted --- qualibrate/qualibration_node.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qualibrate/qualibration_node.py b/qualibrate/qualibration_node.py index 506c47b..63e8feb 100644 --- a/qualibrate/qualibration_node.py +++ b/qualibrate/qualibration_node.py @@ -557,8 +557,10 @@ def serialize(self, **kwargs: Any) -> Mapping[str, Any]: data["parameters"]["qubits"]["metadata"] = metadata except Exception: # for any exception that could happen, return the old data as it was - logger.info("Failed to serialize quam machine, probably used a different quam" - "package hence the machine json load isn't compatible") + logger.info( + "Failed to serialize quam machine, probably used a different quam" + "package hence the machine json load isn't compatible" + ) return pre_mutated_data return data From 8a8372c44dd592c7d1b98c0f025ed24d0224c733 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Sun, 28 Dec 2025 19:43:24 +0200 Subject: [PATCH 54/73] linted --- qualibrate/qualibration_node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qualibrate/qualibration_node.py b/qualibrate/qualibration_node.py index 63e8feb..81a3bc6 100644 --- a/qualibrate/qualibration_node.py +++ b/qualibrate/qualibration_node.py @@ -558,8 +558,8 @@ def serialize(self, **kwargs: Any) -> Mapping[str, Any]: except Exception: # for any exception that could happen, return the old data as it was logger.info( - "Failed to serialize quam machine, probably used a different quam" - "package hence the machine json load isn't compatible" + "Failed to serialize quam machine, probably used a different" + " quam package hence the machine json load isn't compatible" ) return pre_mutated_data return data From b949eba795a3cfbb0dbdee78cb3fc4320b33d064 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Sun, 28 Dec 2025 19:54:12 +0200 Subject: [PATCH 55/73] fix tests for ci, was a wrong import --- .../test_qubit_inside_node_serialization.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py b/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py index 7119c24..45f6de9 100644 --- a/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py +++ b/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py @@ -1,10 +1,8 @@ from typing import Any from unittest.mock import Mock - import pytest - -from qualibrate import QualibrationNode -from qualibrate.parameters import NodeParameters +from qualibrate.qualibration_node import QualibrationNode +from qualibrate import NodeParameters class MockQubit: From 7ea9170f67ed566e5891c1a82cec8d638099e3d4 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Sun, 28 Dec 2025 19:57:37 +0200 Subject: [PATCH 56/73] linted --- .../test_qubit_inside_node_serialization.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py b/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py index 45f6de9..862b813 100644 --- a/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py +++ b/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py @@ -1,8 +1,10 @@ from typing import Any from unittest.mock import Mock + import pytest -from qualibrate.qualibration_node import QualibrationNode + from qualibrate import NodeParameters +from qualibrate.qualibration_node import QualibrationNode class MockQubit: From 24a01eadb282078ad75e2bbb656c5de368a6f475 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Sun, 28 Dec 2025 20:10:00 +0200 Subject: [PATCH 57/73] trying to fix testing for ci --- .../test_qubit_inside_node_serialization.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py b/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py index 862b813..97488d2 100644 --- a/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py +++ b/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py @@ -37,8 +37,9 @@ class TestNodeSerialization: """Test suite for QualibrationNode.serialize() method""" @pytest.fixture - def base_node(self): + def base_node(self, mocker): """Create a basic node without machine for testing""" + mocker.patch.object(QualibrationNode, "_get_storage_manager") node = QualibrationNode(name="test_node", parameters=NodeParameters()) return node @@ -227,8 +228,9 @@ def test_serialize_metadata_structure(self, node_with_machine): class TestNodeSerializationEdgeCases: """Test edge cases and error conditions""" - def test_serialize_with_partial_machine_attributes(self): + def test_serialize_with_partial_machine_attributes(self, mocker): """Test machine with only some attributes""" + mocker.patch.object(QualibrationNode, "_get_storage_manager") node = QualibrationNode(name="test", parameters=NodeParameters()) # Machine with only active_qubits @@ -246,8 +248,9 @@ def test_serialize_with_partial_machine_attributes(self): result = node.serialize() assert isinstance(result, dict) - def test_serialize_large_number_of_qubits(self): + def test_serialize_large_number_of_qubits(self, mocker): """Test serialization with a large number of qubits""" + mocker.patch.object(QualibrationNode, "_get_storage_manager") node = QualibrationNode(name="test", parameters=NodeParameters()) # Create 100 qubits From 4b876a0b1e10dca4e05c07a6607e435f756e46b5 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Sun, 28 Dec 2025 20:19:57 +0200 Subject: [PATCH 58/73] linted --- tests/calibration_nodes_and_graphs_for_testing/demo_node.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/calibration_nodes_and_graphs_for_testing/demo_node.py b/tests/calibration_nodes_and_graphs_for_testing/demo_node.py index e42a00d..8d821bf 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/demo_node.py +++ b/tests/calibration_nodes_and_graphs_for_testing/demo_node.py @@ -1,7 +1,5 @@ from enum import Enum -from pydantic import Field - from qualibrate import NodeParameters, QualibrationNode from qualibrate.models.outcome import Outcome From 5631212ff6a5a5ac9427d72082d10619d3db28bb Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Sun, 28 Dec 2025 20:29:13 +0200 Subject: [PATCH 59/73] fixed conflict --- tests/unit/test_orchestration/test_basic_orchestrator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_orchestration/test_basic_orchestrator.py b/tests/unit/test_orchestration/test_basic_orchestrator.py index 77dd31e..2994419 100644 --- a/tests/unit/test_orchestration/test_basic_orchestrator.py +++ b/tests/unit/test_orchestration/test_basic_orchestrator.py @@ -5,7 +5,7 @@ from qualibrate import QualibrationNode from qualibrate.models.node_status import ElementRunStatus -from qualibrate.models.operational_condition import OperationalCondition +from qualibrate.models.operational_condition import OperationalCondition, LoopCondition from qualibrate.models.outcome import Outcome from qualibrate.models.run_summary.node import NodeRunSummary from qualibrate.orchestration.basic_orchestrator import BasicOrchestrator From 5acf08d81fce193de0807a260b1bc36d7ad5e4e4 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Sun, 28 Dec 2025 20:30:19 +0200 Subject: [PATCH 60/73] linted --- tests/unit/test_orchestration/test_basic_orchestrator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_orchestration/test_basic_orchestrator.py b/tests/unit/test_orchestration/test_basic_orchestrator.py index 2994419..a0b4f7e 100644 --- a/tests/unit/test_orchestration/test_basic_orchestrator.py +++ b/tests/unit/test_orchestration/test_basic_orchestrator.py @@ -5,7 +5,10 @@ from qualibrate import QualibrationNode from qualibrate.models.node_status import ElementRunStatus -from qualibrate.models.operational_condition import OperationalCondition, LoopCondition +from qualibrate.models.operational_condition import ( + LoopCondition, + OperationalCondition, +) from qualibrate.models.outcome import Outcome from qualibrate.models.run_summary.node import NodeRunSummary from qualibrate.orchestration.basic_orchestrator import BasicOrchestrator From 807d5a9357351f8213a6d7a578e97a3ad98b9e8f Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Mon, 29 Dec 2025 10:10:40 +0200 Subject: [PATCH 61/73] added validation, need to fix test and add new tests --- qualibrate/qualibration_graph.py | 34 +++++++++++++++---- ...graph_for_tests_with_connect_on_failure.py | 2 ++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/qualibrate/qualibration_graph.py b/qualibrate/qualibration_graph.py index 86f3913..8d5144f 100644 --- a/qualibrate/qualibration_graph.py +++ b/qualibrate/qualibration_graph.py @@ -316,16 +316,24 @@ def _add_nodes_and_connections_to_nx(self) -> None: to: - Add elements to the graph based on their names. - Add edges (connections) between elements if both elements exist. + - Validate that every element with outgoing connections has at least one + connection on a successful outcome. Raises: ValueError: If a connection references an element that has not been registered. The error message includes the offending element name and the available element names. + ValueError: If an element has outgoing connections but no connection + defined for a successful outcome. """ for element_name in self._elements: self._add_element_to_nx_by_name(element_name) + # Track which nodes have success connections + nodes_with_success = set() + nodes_with_any_connection = set() + for source, destination in self._connectivity: try: source_element = self._get_element_or_error(source) @@ -344,17 +352,31 @@ def _add_nodes_and_connections_to_nx(self) -> None: "registered. Available element names: " f"{tuple(self._elements.keys())}" ) from ex + + # Track connections + nodes_with_any_connection.add(source) + edge_data = self._connectivity[(source, destination)] + if edge_data[self.RUN_SCENARIO_FIELD] == Outcome.SUCCESSFUL: + nodes_with_success.add(source) + if not self._graph.has_edge(source_element, destination_element): self._graph.add_edge( source_element, destination_element, - scenario=self._connectivity[(source, destination)][ - QualibrationGraph.RUN_SCENARIO_FIELD - ], - operational_condition=self._connectivity[ - (source, destination) - ][QualibrationGraph.OPERATIONAL_CONDITION_FIELD], + scenario=edge_data[self.RUN_SCENARIO_FIELD], + operational_condition=edge_data[self.OPERATIONAL_CONDITION_FIELD], ) + + # Validate that all nodes with connections have at least one success path + nodes_without_success = nodes_with_any_connection - nodes_with_success + if nodes_without_success: + raise ValueError( + f"The following nodes in graph '{self.name}' have outgoing " + f"connections but no success path: {sorted(nodes_without_success)}. " + f"Every node with outgoing edges must have at least one connection " + f"on successful outcome." + ) + self._validate_graph_acyclic() @staticmethod diff --git a/tests/unit/test_qualibration_graph/calibrations/nested_graph_for_tests_with_connect_on_failure.py b/tests/unit/test_qualibration_graph/calibrations/nested_graph_for_tests_with_connect_on_failure.py index a73e52e..24581a1 100644 --- a/tests/unit/test_qualibration_graph/calibrations/nested_graph_for_tests_with_connect_on_failure.py +++ b/tests/unit/test_qualibration_graph/calibrations/nested_graph_for_tests_with_connect_on_failure.py @@ -35,7 +35,9 @@ class Parameters(GraphParameters): ) as subg: subg.add_node(library.nodes["test_cal"]) subg.add_node(library.nodes["one_more_node"]) + subg.add_node(library.nodes["test_node"]) subg.connect_on_failure(src="test_cal", dst="one_more_node") + subg.connect_on_failure(src="test_cal", dst="test_node") node1 = library.nodes["test_cal"] From 2368cf9f6d2f5c5a40b77adebbe78dc70f128cf3 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Mon, 29 Dec 2025 11:03:59 +0200 Subject: [PATCH 62/73] ready for merge --- qualibrate/qualibration_graph.py | 4 +- ...graph_for_tests_with_connect_on_failure.py | 2 +- .../test_qualibration_graph/test_graph.py | 63 +++++++++++++++++++ .../test_graph_serialize.py | 14 ++++- 4 files changed, 80 insertions(+), 3 deletions(-) diff --git a/qualibrate/qualibration_graph.py b/qualibrate/qualibration_graph.py index 8d5144f..f8eacb6 100644 --- a/qualibrate/qualibration_graph.py +++ b/qualibrate/qualibration_graph.py @@ -364,7 +364,9 @@ def _add_nodes_and_connections_to_nx(self) -> None: source_element, destination_element, scenario=edge_data[self.RUN_SCENARIO_FIELD], - operational_condition=edge_data[self.OPERATIONAL_CONDITION_FIELD], + operational_condition=edge_data[ + self.OPERATIONAL_CONDITION_FIELD + ], ) # Validate that all nodes with connections have at least one success path diff --git a/tests/unit/test_qualibration_graph/calibrations/nested_graph_for_tests_with_connect_on_failure.py b/tests/unit/test_qualibration_graph/calibrations/nested_graph_for_tests_with_connect_on_failure.py index 24581a1..f6a1241 100644 --- a/tests/unit/test_qualibration_graph/calibrations/nested_graph_for_tests_with_connect_on_failure.py +++ b/tests/unit/test_qualibration_graph/calibrations/nested_graph_for_tests_with_connect_on_failure.py @@ -37,7 +37,7 @@ class Parameters(GraphParameters): subg.add_node(library.nodes["one_more_node"]) subg.add_node(library.nodes["test_node"]) subg.connect_on_failure(src="test_cal", dst="one_more_node") - subg.connect_on_failure(src="test_cal", dst="test_node") + subg.connect(src="test_cal", dst="test_node") node1 = library.nodes["test_cal"] diff --git a/tests/unit/test_qualibration_graph/test_graph.py b/tests/unit/test_qualibration_graph/test_graph.py index 7284789..743afc7 100644 --- a/tests/unit/test_qualibration_graph/test_graph.py +++ b/tests/unit/test_qualibration_graph/test_graph.py @@ -670,3 +670,66 @@ def test__validate_graph_acyclic_without_cycle( orchestrator=mock_orchestrator, ) graph._validate_graph_acyclic() + + def test_validate_success_paths_raises_error_when_no_success_connection( + self, pre_setup_graph_nodes, mock_library + ): + """Test that graph raises ValueError when a node has only failure connections.""" + nodes = pre_setup_graph_nodes + + # Create a third node + node3 = MagicMock(QualibrationNode) + node3.name = "node3" + # Mocking parameters class to be independent on + # classes to get an error for the right reasons + nodes["node1"].parameters_class = MagicMock() + nodes["node2"].parameters_class = MagicMock() + node3.parameters_class = MagicMock() + + with ( + pytest.raises(ValueError, match="no success path.*node1"), + QualibrationGraph.build( + name="test_graph", + parameters=GraphParameters(), + ) as graph, + ): + graph.add_nodes(nodes["node1"], nodes["node2"], node3) + # Only add failure connections from node1 + graph.connect_on_failure("node1", "node2") + graph.connect_on_failure("node1", node3) + # node2 has a success connection, so it's fine + graph.connect("node2", node3) + + def test_validate_success_paths_allows_mixed_connections( + self, mocker, pre_setup_graph_nodes, mock_library + ): + """Test that graph allows both success and failure connections from same node.""" + nodes = pre_setup_graph_nodes + + # Create a third node + node3 = MagicMock(QualibrationNode) + node3.name = "node3" + nodes["node3"] = node3 + + # Mock _build_parameters_class to avoid pydantic issues + mocker.patch.object( + QualibrationGraph, + "_build_parameters_class", + return_value=MagicMock(), + ) + + # Should not raise - node1 has at least one success path + with QualibrationGraph.build( + name="test_graph", + parameters=GraphParameters(), + ) as graph: + graph.add_nodes(nodes["node1"], nodes["node2"], nodes["node3"]) + graph.connect("node1", "node2") # Success path + graph.connect_on_failure("node1", "node3") # Failure path + graph.connect("node2", "node3") # Another valid connection + + assert graph._finalized + assert len(graph._connectivity) == 3 + # Verify node1 has both success and failure connections + assert ("node1", "node2") in graph._connectivity + assert ("node1", "node3") in graph._connectivity diff --git a/tests/unit/test_qualibration_graph/test_graph_serialize.py b/tests/unit/test_qualibration_graph/test_graph_serialize.py index 35b9851..2935824 100644 --- a/tests/unit/test_qualibration_graph/test_graph_serialize.py +++ b/tests/unit/test_qualibration_graph/test_graph_serialize.py @@ -197,6 +197,10 @@ def test_serialize_with_nested_graphs_and_connect_on_failure( "name": "one_more_node", "data": {"label": "one_more_node"}, }, + { + "name": "test_node", + "data": {"label": "test_node"}, + }, ], "edges": [ { @@ -206,7 +210,15 @@ def test_serialize_with_nested_graphs_and_connect_on_failure( "data": { "connect_on": False, }, - } + }, + { + "id": "test_cal->test_node", + "source": "test_cal", + "target": "test_node", + "data": { + "connect_on": True, + }, + }, ], }, }, From 27f87ba2401f94a164df91b5b9733966316f20d8 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Mon, 29 Dec 2025 11:07:56 +0200 Subject: [PATCH 63/73] ready for merge --- tests/unit/test_qualibration_graph/test_graph.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/unit/test_qualibration_graph/test_graph.py b/tests/unit/test_qualibration_graph/test_graph.py index 743afc7..44ea727 100644 --- a/tests/unit/test_qualibration_graph/test_graph.py +++ b/tests/unit/test_qualibration_graph/test_graph.py @@ -680,11 +680,12 @@ def test_validate_success_paths_raises_error_when_no_success_connection( # Create a third node node3 = MagicMock(QualibrationNode) node3.name = "node3" + nodes["node3"] = node3 # Mocking parameters class to be independent on # classes to get an error for the right reasons nodes["node1"].parameters_class = MagicMock() nodes["node2"].parameters_class = MagicMock() - node3.parameters_class = MagicMock() + nodes["node3"].parameters_class = MagicMock() with ( pytest.raises(ValueError, match="no success path.*node1"), @@ -693,12 +694,12 @@ def test_validate_success_paths_raises_error_when_no_success_connection( parameters=GraphParameters(), ) as graph, ): - graph.add_nodes(nodes["node1"], nodes["node2"], node3) + graph.add_nodes(nodes["node1"], nodes["node2"], nodes["node3"]) # Only add failure connections from node1 graph.connect_on_failure("node1", "node2") - graph.connect_on_failure("node1", node3) + graph.connect_on_failure("node1", "node3") # node2 has a success connection, so it's fine - graph.connect("node2", node3) + graph.connect("node2", "node3") def test_validate_success_paths_allows_mixed_connections( self, mocker, pre_setup_graph_nodes, mock_library @@ -729,7 +730,7 @@ def test_validate_success_paths_allows_mixed_connections( graph.connect("node2", "node3") # Another valid connection assert graph._finalized - assert len(graph._connectivity) == 3 + assert len(graph._connectivity) == len(nodes) # Verify node1 has both success and failure connections assert ("node1", "node2") in graph._connectivity assert ("node1", "node3") in graph._connectivity From f0009d33766991c94f3c2d2ff533e40cf79ea624 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Tue, 30 Dec 2025 17:04:05 +0200 Subject: [PATCH 64/73] first version of qubits for graphs --- qualibrate/qualibration_graph.py | 25 +++++++++ qualibrate/qualibration_node.py | 2 + qualibrate/utils/graph_building.py | 37 ++++++++++++- .../demo_node_with_mocked_machine.py | 52 +++++++++++++++++++ .../long_graph.py | 16 +++--- .../long_graph_sub_graph.py | 46 ++++++++++++++++ 6 files changed, 170 insertions(+), 8 deletions(-) create mode 100644 tests/calibration_nodes_and_graphs_for_testing/demo_node_with_mocked_machine.py create mode 100644 tests/calibration_nodes_and_graphs_for_testing/long_graph_sub_graph.py diff --git a/qualibrate/qualibration_graph.py b/qualibrate/qualibration_graph.py index 86f3913..d856c9e 100644 --- a/qualibrate/qualibration_graph.py +++ b/qualibrate/qualibration_graph.py @@ -985,6 +985,31 @@ def __serialize_data(self, /, **kwargs: Any) -> Mapping[str, Any]: node.update(additional) connectivity.extend([(node_id, item["id"]) for item in adjacency]) data.update({"nodes": nodes, "connectivity": connectivity}) + #change qubits to a var, also add cast to what we send into the method + add_metadata = True + for element in self._elements.values(): + if isinstance(element, QualibrationNode): + if element.machine is None: + add_metadata = False + break + elif isinstance(element, QualibrationGraph): + try: + value = additional["parameters"]["qubits"]["metadata"] + except KeyError: + add_metadata = False + break + else: + add_metadata = False + break + if add_metadata and "qubits" in data["parameters"]: + element = next(iter(self._elements.values())) + machine = None + if isinstance(element, QualibrationNode): + data["parameters"]["qubits"]["metadata"] = self.__class__._get_machine_for_graph( + element.machine + ) + elif isinstance(element, QualibrationGraph): + data["parameters"]["qubits"]["metadata"] = additional["parameters"]["qubits"]["metadata"] if cytoscape: data["cytoscape"] = self.__class__.cytoscape_representation(data) return data diff --git a/qualibrate/qualibration_node.py b/qualibrate/qualibration_node.py index 81a3bc6..c6aebab 100644 --- a/qualibrate/qualibration_node.py +++ b/qualibrate/qualibration_node.py @@ -265,12 +265,14 @@ def __copy__(self) -> Self: """ modes = self.modes.model_copy(update={"inspection": False}) active_node = self.__class__.active_node + try: self.__class__.active_node = None instance = self.__class__( self.name, self.parameters_class(), self.description, + machine=self.machine, modes=modes, ) instance.modes.inspection = self.modes.inspection diff --git a/qualibrate/utils/graph_building.py b/qualibrate/utils/graph_building.py index 7b91ee0..03a9fe4 100644 --- a/qualibrate/utils/graph_building.py +++ b/qualibrate/utils/graph_building.py @@ -9,7 +9,7 @@ TypeVar, cast, ) - +from qualibrate.utils.logger_m import logger import networkx as nx from qualibrate.parameters import RunnableParameters @@ -114,6 +114,41 @@ def nx_graph_export( return data @staticmethod + def _get_machine_for_graph(machine): + # Build metadata for each qubit + metadata = {} + try: + if ( + machine is not None + and hasattr(machine, "active_qubits") + and hasattr(machine, "qubits") + ): + qubits = machine.qubits.keys() + active_qubits = { + qubit.name for qubit in machine.active_qubits + } + + for qubit in qubits: + qubit_info = machine.qubits[qubit] + gate_fidelity = None + if hasattr(qubit_info, "gate_fidelity") and hasattr( + qubit_info.gate_fidelity, "averaged" + ): + gate_fidelity = qubit_info.gate_fidelity.averaged + metadata[qubit] = { + "active": qubit in active_qubits, + "fidelity": gate_fidelity, + } + + except Exception: + # for any exception that could happen, return the old data as it was + logger.info( + "Failed to serialize quam machine for graph, probably used a different" + " quam package hence the machine json load isn't compatible" + ) + return {} + return metadata + @staticmethod def cytoscape_representation( serialized: Mapping[str, Any], ) -> Sequence[Mapping[str, Any]]: diff --git a/tests/calibration_nodes_and_graphs_for_testing/demo_node_with_mocked_machine.py b/tests/calibration_nodes_and_graphs_for_testing/demo_node_with_mocked_machine.py new file mode 100644 index 0000000..b766e7a --- /dev/null +++ b/tests/calibration_nodes_and_graphs_for_testing/demo_node_with_mocked_machine.py @@ -0,0 +1,52 @@ +from enum import Enum + +from qualibrate import NodeParameters, QualibrationNode + +class FakeGateFidelity: + def __init__(self, averaged: float): + self.averaged = averaged + +class FakeQubit: + def __init__(self, name: str, gate_fidelity: FakeGateFidelity | None = None): + self.name = name + self.gate_fidelity = gate_fidelity + +class FakeMachine: + def __init__(self): + # qubits dict + self.qubits = { + "q1": FakeQubit("q1", FakeGateFidelity(0.99)), + "q2": FakeQubit("q2", FakeGateFidelity(0.95)), + "q3": FakeQubit("q3") # no gate fidelity + } + # active_qubits list + self.active_qubits = [ + self.qubits["q1"], + self.qubits["q3"] + ] + +class Color(Enum): + RED = "red" + GREEN = "green" + BLUE = "blue" + + +class Parameters(NodeParameters): + qubits: list[str] = ["q1", "q2", "q3", "q4"] + str_value: str = "test" + int_value: int = 1 + float_value: float = 1 + random_color: Color = Color.BLUE + +node = QualibrationNode("test_node_mocked_machine", parameters=Parameters(), machine=FakeMachine() +) + + +@node.run_action() +def node_runs_indication(node: QualibrationNode): + + node.log("node is running") + node.machine = FakeMachine() + result = node.serialize() + print(result) + return result \ No newline at end of file diff --git a/tests/calibration_nodes_and_graphs_for_testing/long_graph.py b/tests/calibration_nodes_and_graphs_for_testing/long_graph.py index fd35f04..becbc47 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/long_graph.py +++ b/tests/calibration_nodes_and_graphs_for_testing/long_graph.py @@ -4,23 +4,25 @@ library = QualibrationLibrary.get_active_library() +USED_NODE = "test_node_mocked_machine" class Parameters(GraphParameters): - targets_name: ClassVar[str] = "qubits_1" - qubits_1: list[str] + targets_name: ClassVar[str] = "qubits" + qubits: list[str] with QualibrationGraph.build( "long_graph", - parameters=Parameters(qubits_1=[f"q{i}" for i in range(3)]), + parameters=Parameters(qubits=[f"q{i}" for i in range(3)]), ) as graph: - graph.add_node(library.nodes.get_nocopy("test_node").copy(name="node_1")) - for i in range(2, 150): + graph.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node_1")) + for i in range(2, 5): graph.add_node( - library.nodes.get_nocopy("test_node").copy(name=f"node_{i}") + library.nodes.get_nocopy(USED_NODE).copy(name=f"node_{i}") ) graph.connect(src=f"node_{i - 1}", dst=f"node_{i}") if __name__ == "__main__": - result = graph.run() + # result = graph.run() + result = graph.serialize() print(result) diff --git a/tests/calibration_nodes_and_graphs_for_testing/long_graph_sub_graph.py b/tests/calibration_nodes_and_graphs_for_testing/long_graph_sub_graph.py new file mode 100644 index 0000000..3289618 --- /dev/null +++ b/tests/calibration_nodes_and_graphs_for_testing/long_graph_sub_graph.py @@ -0,0 +1,46 @@ +from typing import ClassVar +from qualibrate import GraphParameters, QualibrationGraph, QualibrationLibrary + +library = QualibrationLibrary.get_active_library() +USED_NODE = "test_node_mocked_machine" + +class Parameters(GraphParameters): + targets_name: ClassVar[str] = "qubits" + qubits: list[str] + +# Build the main graph +with QualibrationGraph.build( + "long_graph_sub_graph", + parameters=Parameters(qubits=[f"q{i}" for i in range(3)]), +) as graph: + + # # Add nodes to the main graph + # graph.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node_1")) + # for i in range(2, 5): + # graph.add_node( + # library.nodes.get_nocopy(USED_NODE).copy(name=f"node_{i}") + # ) + # graph.connect(src=f"node_{i - 1}", dst=f"node_{i}") + + # Build the subgraph that repeats the same structure + with QualibrationGraph.build( + "repeated_subgraph", + parameters=Parameters(qubits=[f"q{i}" for i in range(3)]), + ) as subg: + subg.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node_1")) + # for i in range(2, 5): + # subg.add_node( + # library.nodes.get_nocopy(USED_NODE).copy(name=f"node_{i}") + # ) + # subg.connect(src=f"node_{i - 1}", dst=f"node_{i}") + + # Add the subgraph as a node in the main graph + graph.add_node(subg) + + # Connect last node of main graph to subgraph + # graph.connect(src="node_4", dst=subg) + +if __name__ == "__main__": + # result = graph.run() + result = graph.serialize() + print(result) \ No newline at end of file From 8fbf4faad6ff4b7d213c6f724da9d613f48174d5 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Wed, 31 Dec 2025 10:03:12 +0200 Subject: [PATCH 65/73] push before major changes, will add tests and refactor method --- qualibrate/qualibration_graph.py | 3 +- qualibrate/qualibration_node.py | 8 +- .../long_graph_sub_graph.py | 24 +++--- .../test_qubit_inside_node_serialization.py | 82 +++++++++++++------ 4 files changed, 74 insertions(+), 43 deletions(-) diff --git a/qualibrate/qualibration_graph.py b/qualibrate/qualibration_graph.py index d856c9e..d0e7e21 100644 --- a/qualibrate/qualibration_graph.py +++ b/qualibrate/qualibration_graph.py @@ -994,7 +994,7 @@ def __serialize_data(self, /, **kwargs: Any) -> Mapping[str, Any]: break elif isinstance(element, QualibrationGraph): try: - value = additional["parameters"]["qubits"]["metadata"] + _ = additional["parameters"]["qubits"]["metadata"] except KeyError: add_metadata = False break @@ -1003,7 +1003,6 @@ def __serialize_data(self, /, **kwargs: Any) -> Mapping[str, Any]: break if add_metadata and "qubits" in data["parameters"]: element = next(iter(self._elements.values())) - machine = None if isinstance(element, QualibrationNode): data["parameters"]["qubits"]["metadata"] = self.__class__._get_machine_for_graph( element.machine diff --git a/qualibrate/qualibration_node.py b/qualibrate/qualibration_node.py index c6aebab..48fa7ba 100644 --- a/qualibrate/qualibration_node.py +++ b/qualibrate/qualibration_node.py @@ -527,6 +527,8 @@ def serialize(self, **kwargs: Any) -> Mapping[str, Any]: pre_mutated_data = copy.deepcopy(data) try: if ( + "parameters" in data and + "qubits" in data["parameters"] and self.machine is not None and hasattr(self.machine, "active_qubits") and hasattr(self.machine, "qubits") @@ -550,12 +552,6 @@ def serialize(self, **kwargs: Any) -> Mapping[str, Any]: "fidelity": gate_fidelity, } - # Store metadata in the serialized data - if "parameters" not in data: - data["parameters"] = {} - if "qubits" not in data["parameters"]: - data["parameters"]["qubits"] = {} - data["parameters"]["qubits"]["metadata"] = metadata except Exception: # for any exception that could happen, return the old data as it was diff --git a/tests/calibration_nodes_and_graphs_for_testing/long_graph_sub_graph.py b/tests/calibration_nodes_and_graphs_for_testing/long_graph_sub_graph.py index 3289618..db77a62 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/long_graph_sub_graph.py +++ b/tests/calibration_nodes_and_graphs_for_testing/long_graph_sub_graph.py @@ -15,12 +15,12 @@ class Parameters(GraphParameters): ) as graph: # # Add nodes to the main graph - # graph.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node_1")) - # for i in range(2, 5): - # graph.add_node( - # library.nodes.get_nocopy(USED_NODE).copy(name=f"node_{i}") - # ) - # graph.connect(src=f"node_{i - 1}", dst=f"node_{i}") + graph.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node_1")) + for i in range(2, 5): + graph.add_node( + library.nodes.get_nocopy(USED_NODE).copy(name=f"node_{i}") + ) + graph.connect(src=f"node_{i - 1}", dst=f"node_{i}") # Build the subgraph that repeats the same structure with QualibrationGraph.build( @@ -28,17 +28,17 @@ class Parameters(GraphParameters): parameters=Parameters(qubits=[f"q{i}" for i in range(3)]), ) as subg: subg.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node_1")) - # for i in range(2, 5): - # subg.add_node( - # library.nodes.get_nocopy(USED_NODE).copy(name=f"node_{i}") - # ) - # subg.connect(src=f"node_{i - 1}", dst=f"node_{i}") + for i in range(2, 5): + subg.add_node( + library.nodes.get_nocopy(USED_NODE).copy(name=f"node_{i}") + ) + subg.connect(src=f"node_{i - 1}", dst=f"node_{i}") # Add the subgraph as a node in the main graph graph.add_node(subg) # Connect last node of main graph to subgraph - # graph.connect(src="node_4", dst=subg) + graph.connect(src="node_4", dst=subg) if __name__ == "__main__": # result = graph.run() diff --git a/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py b/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py index 97488d2..919d52a 100644 --- a/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py +++ b/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py @@ -33,6 +33,11 @@ def _active_qubits_names(mocked_machine: MockMachine) -> list[str]: return [active_qubit.name for active_qubit in mocked_machine.active_qubits] +# Custom parameters class with qubits field +class ParametersWithQubits(NodeParameters): + qubits: list[str] = [] + + class TestNodeSerialization: """Test suite for QualibrationNode.serialize() method""" @@ -43,6 +48,16 @@ def base_node(self, mocker): node = QualibrationNode(name="test_node", parameters=NodeParameters()) return node + @pytest.fixture + def base_node_with_qubits_params(self, mocker): + """Create a basic node with qubits in parameters""" + mocker.patch.object(QualibrationNode, "_get_storage_manager") + node = QualibrationNode( + name="test_node", + parameters=ParametersWithQubits() + ) + return node + @pytest.fixture def mock_qubits(self): """Create mock qubits with various configurations""" @@ -56,7 +71,7 @@ def mock_qubits(self): return qubits @pytest.fixture - def node_with_machine(self, base_node, mock_qubits): + def node_with_machine(self, base_node_with_qubits_params, mock_qubits): """Create a node with a machine that has qubits""" active_qubits = [ mock_qubits["qA1"], @@ -65,8 +80,8 @@ def node_with_machine(self, base_node, mock_qubits): ] machine = MockMachine(mock_qubits, active_qubits) - base_node.machine = machine - return base_node + base_node_with_qubits_params.machine = machine + return base_node_with_qubits_params def test_serialize_without_machine(self, base_node): """Test serialization when machine is None""" @@ -79,13 +94,26 @@ def test_serialize_without_machine(self, base_node): if "parameters" in result and "qubits" in result["parameters"]: assert "metadata" not in result["parameters"]["qubits"] - def test_serialize_with_machine_no_attributes(self, base_node): + def test_serialize_with_machine_but_no_qubits_in_params(self, base_node, mock_qubits): + """Test serialization when machine exists but parameters don't have qubits field""" + active_qubits = [mock_qubits["qA1"]] + machine = MockMachine(mock_qubits, active_qubits) + base_node.machine = machine + + result = base_node.serialize() + + assert isinstance(result, dict) + # Should not have qubits metadata when parameters don't have qubits field + if "parameters" in result: + assert "qubits" not in result["parameters"] or "metadata" not in result["parameters"].get("qubits", {}) + + def test_serialize_with_machine_no_attributes(self, base_node_with_qubits_params): """Test serialization when machine lacks required attributes""" - base_node.machine = Mock( + base_node_with_qubits_params.machine = Mock( spec=[] ) # Machine without active_qubits or qubits - result = base_node.serialize() + result = base_node_with_qubits_params.serialize() assert isinstance(result, dict) # Should not have qubits metadata when machine lacks attributes @@ -138,22 +166,22 @@ def test_serialize_with_complete_machine( ) def test_serialize_active_qubit_identification( - self, base_node, mock_qubits + self, base_node_with_qubits_params, mock_qubits ): """Test that active qubits are correctly identified""" active_qubits = [mock_qubits["qA1"], mock_qubits["qA3"]] machine = MockMachine(mock_qubits, active_qubits) - base_node.machine = machine + base_node_with_qubits_params.machine = machine - result = base_node.serialize() + result = base_node_with_qubits_params.serialize() metadata = result["parameters"]["qubits"]["metadata"] active_qubits_names = _active_qubits_names(machine) - # Only qA1 and qA3 should be activ + # Only qA1 and qA3 should be active for qubit in metadata: assert (metadata[qubit]["active"]) == (qubit in active_qubits_names) - def test_serialize_qubit_without_gate_fidelity(self, base_node): + def test_serialize_qubit_without_gate_fidelity(self, base_node_with_qubits_params): """Test serialization when qubit doesn't have gate_fidelity attribute""" qubits = { "qA1": Mock(spec=["name"]), # No gate_fidelity attribute @@ -161,15 +189,15 @@ def test_serialize_qubit_without_gate_fidelity(self, base_node): qubits["qA1"].name = "qA1" machine = MockMachine(qubits, []) - base_node.machine = machine + base_node_with_qubits_params.machine = machine - result = base_node.serialize() + result = base_node_with_qubits_params.serialize() metadata = result["parameters"]["qubits"]["metadata"] - assert metadata["qA1"]["active"] not in base_node.machine.active_qubits + assert metadata["qA1"]["active"] is False assert metadata["qA1"]["fidelity"] is None - def test_serialize_qubit_without_averaged_fidelity(self, base_node): + def test_serialize_qubit_without_averaged_fidelity(self, base_node_with_qubits_params): """Test serialization when gate_fidelity doesn't have 'averaged' attribute""" qubits = { @@ -179,19 +207,19 @@ def test_serialize_qubit_without_averaged_fidelity(self, base_node): qubits["qA1"].gate_fidelity = Mock(spec=[]) # No 'averaged' attribute machine = MockMachine(qubits, []) - base_node.machine = machine + base_node_with_qubits_params.machine = machine - result = base_node.serialize() + result = base_node_with_qubits_params.serialize() metadata = result["parameters"]["qubits"]["metadata"] assert metadata["qA1"]["fidelity"] is None - def test_serialize_empty_qubits(self, base_node): + def test_serialize_empty_qubits(self, base_node_with_qubits_params): """Test serialization with empty qubits dictionary""" machine = MockMachine({}, []) - base_node.machine = machine + base_node_with_qubits_params.machine = machine - result = base_node.serialize() + result = base_node_with_qubits_params.serialize() assert "parameters" in result assert "qubits" in result["parameters"] @@ -231,7 +259,10 @@ class TestNodeSerializationEdgeCases: def test_serialize_with_partial_machine_attributes(self, mocker): """Test machine with only some attributes""" mocker.patch.object(QualibrationNode, "_get_storage_manager") - node = QualibrationNode(name="test", parameters=NodeParameters()) + node = QualibrationNode( + name="test", + parameters=ParametersWithQubits() + ) # Machine with only active_qubits node.machine = Mock(spec=["active_qubits"]) @@ -239,6 +270,7 @@ def test_serialize_with_partial_machine_attributes(self, mocker): result = node.serialize() assert isinstance(result, dict) + # Should not crash, metadata should not be added # Machine with only qubits node.machine = Mock(spec=["qubits"]) @@ -247,11 +279,15 @@ def test_serialize_with_partial_machine_attributes(self, mocker): result = node.serialize() assert isinstance(result, dict) + # Should not crash, metadata should not be added def test_serialize_large_number_of_qubits(self, mocker): """Test serialization with a large number of qubits""" mocker.patch.object(QualibrationNode, "_get_storage_manager") - node = QualibrationNode(name="test", parameters=NodeParameters()) + node = QualibrationNode( + name="test", + parameters=ParametersWithQubits() + ) # Create 100 qubits qubits = { @@ -273,4 +309,4 @@ def test_serialize_large_number_of_qubits(self, mocker): assert "q0" in active_qubits_names assert "q1" not in active_qubits_names assert "q50" in active_qubits_names - assert "q51" not in active_qubits_names + assert "q51" not in active_qubits_names \ No newline at end of file From 2348e9b20c25867176f4cd360e97c004b5b8059c Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Wed, 31 Dec 2025 15:16:07 +0200 Subject: [PATCH 66/73] WIP pushing to test ui --- qualibrate/q_runnnable.py | 7 + qualibrate/qualibration_graph.py | 48 +- qualibrate/qualibration_node.py | 55 +- ..._qualibration_graph_qubit_serialization.py | 552 ++++++++++++++++++ 4 files changed, 627 insertions(+), 35 deletions(-) create mode 100644 tests/unit/test_qualibration_graph/test_qualibration_graph_qubit_serialization.py diff --git a/qualibrate/q_runnnable.py b/qualibrate/q_runnnable.py index 4034047..b10667c 100644 --- a/qualibrate/q_runnnable.py +++ b/qualibrate/q_runnnable.py @@ -242,6 +242,13 @@ def run(self, **passed_parameters: Any) -> BaseRunSummary: Returns: tuple[QRunnable, BaseRunSummary]: The executed runnable and summary. """ + pass + @abstractmethod + def _get_machine_metadata(self): + + + + pass @property diff --git a/qualibrate/qualibration_graph.py b/qualibrate/qualibration_graph.py index d0e7e21..613de69 100644 --- a/qualibrate/qualibration_graph.py +++ b/qualibrate/qualibration_graph.py @@ -936,6 +936,26 @@ def _add_loop_to_edge(self, node_name: str) -> dict[str, Any]: "data": data, } + def _get_machine_metadata(self): + # if self._check_machines_metadata_equality(): + if self._elements is None: + logger.warning(f"Graph {self.name} has no elements hence cant extract machine metadata") + return None + elements_machines_metadata = (element._get_machine_metadata() for element + in self._elements.values() if element._get_machine_metadata() is not None) + try: + first = next(elements_machines_metadata) + except StopIteration: + logger.info(f"All elements have no quam machine in graph {self.name}") + return None # all None (or empty) + if all(v == first for v in elements_machines_metadata): + return first + logger.warning(f"Not all machines have the same metadata hence cant choose one in graph {self.name}") + return None + + + + def __serialize_data(self, /, **kwargs: Any) -> Mapping[str, Any]: """ Serializes the graph into a dictionary format. @@ -985,30 +1005,10 @@ def __serialize_data(self, /, **kwargs: Any) -> Mapping[str, Any]: node.update(additional) connectivity.extend([(node_id, item["id"]) for item in adjacency]) data.update({"nodes": nodes, "connectivity": connectivity}) - #change qubits to a var, also add cast to what we send into the method - add_metadata = True - for element in self._elements.values(): - if isinstance(element, QualibrationNode): - if element.machine is None: - add_metadata = False - break - elif isinstance(element, QualibrationGraph): - try: - _ = additional["parameters"]["qubits"]["metadata"] - except KeyError: - add_metadata = False - break - else: - add_metadata = False - break - if add_metadata and "qubits" in data["parameters"]: - element = next(iter(self._elements.values())) - if isinstance(element, QualibrationNode): - data["parameters"]["qubits"]["metadata"] = self.__class__._get_machine_for_graph( - element.machine - ) - elif isinstance(element, QualibrationGraph): - data["parameters"]["qubits"]["metadata"] = additional["parameters"]["qubits"]["metadata"] + machine_metadata = self._get_machine_metadata() + if "qubits" in data["parameters"] and machine_metadata is not None: + data["parameters"]["qubits"]["metadata"] = machine_metadata + if cytoscape: data["cytoscape"] = self.__class__.cytoscape_representation(data) return data diff --git a/qualibrate/qualibration_node.py b/qualibrate/qualibration_node.py index 48fa7ba..d8598ce 100644 --- a/qualibrate/qualibration_node.py +++ b/qualibrate/qualibration_node.py @@ -252,6 +252,15 @@ def action_label(self) -> str | None: def action_label(self, value: str | None) -> None: self._custom_action_label = value + @property + def machine(self) -> NodeMachineType | None: + return self._machine + + @machine.setter + def machine(self, value: NodeMachineType | None) -> None: + self._machine = value + self._machine_metadata = None # invalidate cache + def __copy__(self) -> Self: """ Creates a shallow copy of the node. @@ -521,14 +530,12 @@ def load_from_id( build_params_class=isinstance(caller, type), ) - def serialize(self, **kwargs: Any) -> Mapping[str, Any]: - # Get base QRunnable serialization - data = dict(super().serialize(**kwargs)) - pre_mutated_data = copy.deepcopy(data) + def _extract_machines_metadata(self): + metadata = {} try: if ( - "parameters" in data and - "qubits" in data["parameters"] and + self.parameters is not None and + hasattr(self.parameters,"qubits") and self.machine is not None and hasattr(self.machine, "active_qubits") and hasattr(self.machine, "qubits") @@ -539,7 +546,6 @@ def serialize(self, **kwargs: Any) -> Mapping[str, Any]: } # Build metadata for each qubit - metadata = {} for qubit in qubits: qubit_info = self.machine.qubits[qubit] gate_fidelity = None @@ -551,15 +557,42 @@ def serialize(self, **kwargs: Any) -> Mapping[str, Any]: "active": qubit in active_qubits, "fidelity": gate_fidelity, } - - data["parameters"]["qubits"]["metadata"] = metadata except Exception: # for any exception that could happen, return the old data as it was logger.info( - "Failed to serialize quam machine, probably used a different" + "Failed to set quam machine metadata, probably used a different" " quam package hence the machine json load isn't compatible" ) - return pre_mutated_data + return None + return metadata + + def _get_machine_metadata(self): + if self._machine is None: + return None + + if self._machine_metadata is None: + # Only extract metadata if machine has the right attributes + try: + self._machine_metadata = self._extract_machines_metadata() + except AttributeError: + self._machine_metadata = None + + # Only return metadata if machine exists and has qubits attribute + if not hasattr(self._machine, "qubits"): + return None + + # Always return dict (even empty) if machine has qubits + return self._machine_metadata or {} + + def serialize(self, **kwargs: Any) -> Mapping[str, Any]: + # Get base QRunnable serialization + data = dict(super().serialize(**kwargs)) + + metadata = self._get_machine_metadata() + + if "parameters" in data and "qubits" in data["parameters"] and metadata is not None: + data["parameters"]["qubits"]["metadata"] = metadata + return data def _post_run( diff --git a/tests/unit/test_qualibration_graph/test_qualibration_graph_qubit_serialization.py b/tests/unit/test_qualibration_graph/test_qualibration_graph_qubit_serialization.py new file mode 100644 index 0000000..559144e --- /dev/null +++ b/tests/unit/test_qualibration_graph/test_qualibration_graph_qubit_serialization.py @@ -0,0 +1,552 @@ +from typing import Any +from unittest.mock import Mock + +import pytest + +from qualibrate import GraphParameters, NodeParameters, QualibrationGraph, QualibrationNode + + +class MockQubit: + """Mock qubit class for testing""" + + def __init__(self, name: str, gate_fidelity_value: float | None = None): + self.name = name + if gate_fidelity_value is not None: + self.gate_fidelity = Mock() + self.gate_fidelity.averaged = gate_fidelity_value + else: + self.gate_fidelity = Mock(spec=[]) # No 'averaged' attribute + + +class MockMachine: + """Mock machine class for testing""" + + def __init__( + self, qubits_dict: dict[str, Any], active_qubits_list: list[MockQubit] + ): + self.qubits = qubits_dict + self.active_qubits = active_qubits_list + + +def _active_qubits_names(mocked_machine: MockMachine) -> list[str]: + return [active_qubit.name for active_qubit in mocked_machine.active_qubits] + + +# Custom parameters classes with qubits field +class NodeParametersWithQubits(NodeParameters): + qubits: list[str] = [] + + +class GraphParametersWithQubits(GraphParameters): + qubits: list[str] = [] + + +class TestGraphSerialization: + """Test suite for QualibrationGraph.serialize() qubit metadata""" + + @pytest.fixture + def mock_library(self, mocker): + """Mock the QualibrationLibrary to avoid initialization issues""" + mock_lib = mocker.MagicMock() + mock_lib.nodes.values_nocopy.return_value = [] # Empty library + mocker.patch.object( + QualibrationGraph, + "_get_library", + return_value=mock_lib, + ) + return mock_lib + + @pytest.fixture + def mock_qubits(self): + """Create mock qubits with various configurations""" + qubits = { + "q1": MockQubit("q1", 0.99), + "q2": MockQubit("q2", 0.95), + "q3": MockQubit("q3", 0.98), + "q4": MockQubit("q4", None), # No fidelity + } + return qubits + + @pytest.fixture + def mock_machine(self, mock_qubits): + """Create a mock machine with qubits""" + active_qubits = [mock_qubits["q1"], mock_qubits["q3"]] + return MockMachine(mock_qubits, active_qubits) + + @pytest.fixture + def node_with_machine(self, mocker, mock_machine): + """Create a node with machine and qubits parameters""" + mocker.patch.object(QualibrationNode, "_get_storage_manager") + node = QualibrationNode( + name="test_node", + parameters=NodeParametersWithQubits(qubits=["q1", "q2", "q3"]), + machine=mock_machine + ) + return node + + @pytest.fixture + def node_without_machine(self, mocker): + """Create a node without machine""" + mocker.patch.object(QualibrationNode, "_get_storage_manager") + node = QualibrationNode( + name="test_node", + parameters=NodeParametersWithQubits(qubits=["q1", "q2"]) + ) + return node + + @pytest.fixture + def simple_graph_with_nodes(self, mocker, node_with_machine, mock_library): + """Create a simple graph with nodes that have machines""" + mocker.patch.object(QualibrationNode, "_get_storage_manager") + + node1 = node_with_machine.copy(name="node1") + node2 = node_with_machine.copy(name="node2") + + with QualibrationGraph.build( + "test_graph", + parameters=GraphParametersWithQubits(qubits=["q1", "q2", "q3"]) + ) as graph: + graph.add_node(node1) + graph.add_node(node2) + graph.connect("node1", "node2") + + return graph + + def test_serialize_graph_without_qubits_in_parameters( + self, mocker, node_with_machine, mock_library + ): + """Test graph serialization when parameters don't have qubits field""" + mocker.patch.object(QualibrationNode, "_get_storage_manager") + + node1 = node_with_machine.copy(name="node1") + + with QualibrationGraph.build( + "test_graph", + parameters=GraphParameters() # No qubits field + ) as graph: + graph.add_node(node1) + + result = graph.serialize() + + assert isinstance(result, dict) + # Should not have qubits metadata when parameters don't have qubits + if "parameters" in result: + assert "qubits" not in result["parameters"] or \ + "metadata" not in result["parameters"].get("qubits", {}) + + def test_serialize_graph_with_nodes_having_machines( + self, simple_graph_with_nodes, mock_qubits + ): + """Test graph serialization when all nodes have machines""" + result = simple_graph_with_nodes.serialize() + + assert isinstance(result, dict) + assert "parameters" in result + assert "qubits" in result["parameters"] + assert "metadata" in result["parameters"]["qubits"] + + metadata = result["parameters"]["qubits"]["metadata"] + + # Check all qubits are in metadata + assert set(metadata.keys()) == set(mock_qubits.keys()) + + # Check active qubits + for qubit_name in metadata: + if qubit_name in ["q1", "q3"]: + assert metadata[qubit_name]["active"] is True + else: + assert metadata[qubit_name]["active"] is False + + # Check fidelity values + assert metadata["q1"]["fidelity"] == 0.99 + assert metadata["q2"]["fidelity"] == 0.95 + assert metadata["q3"]["fidelity"] == 0.98 + assert metadata["q4"]["fidelity"] is None + + def test_serialize_graph_with_node_without_machine( + self, mocker, node_with_machine, node_without_machine, mock_library + ): + """Test graph serialization when one node doesn't have machine""" + mocker.patch.object(QualibrationNode, "_get_storage_manager") + + node1 = node_with_machine.copy(name="node1") + node2 = node_without_machine.copy(name="node2") + + with QualibrationGraph.build( + "test_graph", + parameters=GraphParametersWithQubits(qubits=["q1", "q2"]) + ) as graph: + graph.add_node(node1) + graph.add_node(node2) + graph.connect("node1", "node2") + + result = graph.serialize() + + if "parameters" in result and "qubits" in result["parameters"]: + assert result["parameters"]["qubits"]["metadata"] + + def test_serialize_graph_with_different_machines( + self, mocker, mock_qubits, mock_library + ): + """Test graph serialization when nodes have different machines""" + mocker.patch.object(QualibrationNode, "_get_storage_manager") + + # Create two different machines + machine1 = MockMachine(mock_qubits, [mock_qubits["q1"]]) + machine2_qubits = { + "q1": MockQubit("q1", 0.88), # Different fidelity + "q2": MockQubit("q2", 0.92), + } + machine2 = MockMachine(machine2_qubits, [machine2_qubits["q1"]]) + + node1 = QualibrationNode( + name="node1", + parameters=NodeParametersWithQubits(qubits=["q1", "q2"]), + machine=machine1 + ) + + node2 = QualibrationNode( + name="node2", + parameters=NodeParametersWithQubits(qubits=["q1", "q2"]), + machine=machine2 + ) + + with QualibrationGraph.build( + "test_graph", + parameters=GraphParametersWithQubits(qubits=["q1", "q2"]) + ) as graph: + graph.add_node(node1) + graph.add_node(node2) + graph.connect("node1", "node2") + + result = graph.serialize() + + # Should not have metadata because machines are different + if "parameters" in result and "qubits" in result["parameters"]: + assert "metadata" not in result["parameters"]["qubits"] + + def test_serialize_graph_with_single_node( + self, mocker, node_with_machine, mock_library + ): + """Test graph serialization with single node""" + mocker.patch.object(QualibrationNode, "_get_storage_manager") + + node1 = node_with_machine.copy(name="node1") + + with QualibrationGraph.build( + "test_graph", + parameters=GraphParametersWithQubits(qubits=["q1", "q2"]) + ) as graph: + graph.add_node(node1) + + result = graph.serialize() + + assert "parameters" in result + assert "qubits" in result["parameters"] + assert "metadata" in result["parameters"]["qubits"] + + def test_serialize_empty_graph(self, mocker, mock_library): + """Test graph serialization with no nodes""" + mocker.patch.object(QualibrationNode, "_get_storage_manager") + + with QualibrationGraph.build( + "test_graph", + parameters=GraphParametersWithQubits(qubits=["q1", "q2"]) + ) as graph: + pass # No nodes added + + result = graph.serialize() + + # Should not have metadata because no nodes + if "parameters" in result and "qubits" in result["parameters"]: + assert "metadata" not in result["parameters"]["qubits"] + + def test_serialize_graph_metadata_structure(self, simple_graph_with_nodes): + """Test the structure of metadata in graph serialization""" + result = simple_graph_with_nodes.serialize() + metadata = result["parameters"]["qubits"]["metadata"] + + for qubit_data in metadata.values(): + # Each qubit should have exactly these keys + assert set(qubit_data.keys()) == {"active", "fidelity"} + # active should be boolean + assert isinstance(qubit_data["active"], bool) + # fidelity should be float or None + assert qubit_data["fidelity"] is None or isinstance( + qubit_data["fidelity"], float + ) + + def test_serialize_graph_preserves_base_data(self, simple_graph_with_nodes): + """Test that serialize preserves all base graph serialization data""" + result = simple_graph_with_nodes.serialize() + + # Should have base fields + assert "name" in result + assert result["name"] == "test_graph" + assert "parameters" in result + assert "nodes" in result + assert "connectivity" in result + + def test_serialize_graph_with_nodes_no_qubits_attribute( + self, mocker, mock_library + ): + """Test graph serialization when node parameters don't have qubits""" + mocker.patch.object(QualibrationNode, "_get_storage_manager") + + node1 = QualibrationNode( + name="node1", + parameters=NodeParameters() # No qubits field + ) + + with QualibrationGraph.build( + "test_graph", + parameters=GraphParametersWithQubits(qubits=["q1"]) + ) as graph: + graph.add_node(node1) + + result = graph.serialize() + + # Should not have metadata because node doesn't have qubits in parameters + if "parameters" in result and "qubits" in result["parameters"]: + assert "metadata" not in result["parameters"]["qubits"] + + +class TestNestedGraphSerialization: + """Test suite for nested graph (subgraph) serialization""" + + @pytest.fixture + def mock_library(self, mocker): + """Mock the QualibrationLibrary""" + mock_lib = mocker.MagicMock() + mock_lib.nodes.values_nocopy.return_value = [] + mocker.patch.object( + QualibrationGraph, + "_get_library", + return_value=mock_lib, + ) + return mock_lib + + @pytest.fixture + def mock_qubits(self): + """Create mock qubits""" + qubits = { + "q1": MockQubit("q1", 0.99), + "q2": MockQubit("q2", 0.95), + } + return qubits + + @pytest.fixture + def mock_machine(self, mock_qubits): + """Create a mock machine""" + active_qubits = [mock_qubits["q1"]] + return MockMachine(mock_qubits, active_qubits) + + @pytest.fixture + def node_with_machine(self, mocker, mock_machine): + """Create a node with machine""" + mocker.patch.object(QualibrationNode, "_get_storage_manager") + node = QualibrationNode( + name="test_node", + parameters=NodeParametersWithQubits(qubits=["q1", "q2"]), + machine=mock_machine + ) + return node + + @pytest.fixture + def subgraph_with_nodes(self, mocker, node_with_machine, mock_library): + """Create a subgraph with nodes""" + mocker.patch.object(QualibrationNode, "_get_storage_manager") + + node1 = node_with_machine.copy(name="subnode1") + node2 = node_with_machine.copy(name="subnode2") + + with QualibrationGraph.build( + "subgraph", + parameters=GraphParametersWithQubits(qubits=["q1", "q2"]) + ) as subgraph: + subgraph.add_node(node1) + subgraph.add_node(node2) + subgraph.connect("subnode1", "subnode2") + + return subgraph + + def test_serialize_graph_with_subgraph( + self, mocker, node_with_machine, subgraph_with_nodes, mock_qubits, mock_library + ): + """Test serialization of graph containing subgraph""" + mocker.patch.object(QualibrationNode, "_get_storage_manager") + + node1 = node_with_machine.copy(name="node1") + + with QualibrationGraph.build( + "parent_graph", + parameters=GraphParametersWithQubits(qubits=["q1", "q2"]) + ) as parent_graph: + parent_graph.add_node(node1) + parent_graph.add_node(subgraph_with_nodes) + parent_graph.connect("node1", "subgraph") + + result = parent_graph.serialize() + + # Parent graph should have metadata + assert "parameters" in result + assert "qubits" in result["parameters"] + assert "metadata" in result["parameters"]["qubits"] + + metadata = result["parameters"]["qubits"]["metadata"] + assert set(metadata.keys()) == set(mock_qubits.keys()) + + def test_serialize_graph_with_subgraph_no_metadata( + self, mocker, node_with_machine, mock_qubits, mock_library + ): + """Test serialization when subgraph has no metadata""" + mocker.patch.object(QualibrationNode, "_get_storage_manager") + + # Create subgraph with node without machine + node_no_machine = QualibrationNode( + name="subnode1", + parameters=NodeParametersWithQubits(qubits=["q1"]) + ) + + with QualibrationGraph.build( + "subgraph", + parameters=GraphParametersWithQubits(qubits=["q1"]) + ) as subgraph: + subgraph.add_node(node_no_machine) + + node1 = node_with_machine.copy(name="node1") + + with QualibrationGraph.build( + "parent_graph", + parameters=GraphParametersWithQubits(qubits=["q1", "q2"]) + ) as parent_graph: + parent_graph.add_node(node1) + parent_graph.add_node(subgraph) + parent_graph.connect("node1", "subgraph") + + result = parent_graph.serialize() + + # Parent graph should not have metadata because subgraph doesn't + if "parameters" in result and "qubits" in result["parameters"]: + assert "metadata" not in result["parameters"]["qubits"] + + +class TestGraphSerializationEdgeCases: + """Test edge cases for graph serialization""" + + @pytest.fixture + def mock_library(self, mocker): + """Mock the QualibrationLibrary""" + mock_lib = mocker.MagicMock() + mock_lib.nodes.values_nocopy.return_value = [] + mocker.patch.object( + QualibrationGraph, + "_get_library", + return_value=mock_lib, + ) + return mock_lib + + @pytest.fixture + def mock_machine(self): + """Create a mock machine""" + qubits = { + "q1": MockQubit("q1", 0.99), + } + return MockMachine(qubits, [qubits["q1"]]) + + def test_serialize_graph_with_many_identical_nodes( + self, mocker, mock_machine, mock_library + ): + """Test serialization with many nodes having identical machines""" + mocker.patch.object(QualibrationNode, "_get_storage_manager") + + # Create multiple nodes with same machine + nodes = [] + for i in range(10): + node = QualibrationNode( + name=f"node{i}", + parameters=NodeParametersWithQubits(qubits=["q1"]), + machine=mock_machine + ) + nodes.append(node) + + with QualibrationGraph.build( + "test_graph", + parameters=GraphParametersWithQubits(qubits=["q1"]) + ) as graph: + for node in nodes: + graph.add_node(node) + + # Connect in chain + for i in range(len(nodes) - 1): + graph.connect(f"node{i}", f"node{i + 1}") + + result = graph.serialize() + + # Should have metadata since all machines are identical + assert "parameters" in result + assert "qubits" in result["parameters"] + assert "metadata" in result["parameters"]["qubits"] + assert result["parameters"]["qubits"]["metadata"]["q1"]["fidelity"] == 0.99 + + def test_serialize_graph_with_machine_missing_attributes( + self, mocker, mock_library + ): + """Test serialization when machine is missing some attributes""" + mocker.patch.object(QualibrationNode, "_get_storage_manager") + + # Machine with only qubits, no active_qubits + machine = Mock(spec=["qubits"]) + machine.qubits = {"q1": MockQubit("q1", 0.99)} + + node = QualibrationNode( + name="node1", + parameters=NodeParametersWithQubits(qubits=["q1"]), + machine=machine + ) + + with QualibrationGraph.build( + "test_graph", + parameters=GraphParametersWithQubits(qubits=["q1"]) + ) as graph: + graph.add_node(node) + + result = graph.serialize() + + # Should not crash but also should not have metadata + assert isinstance(result, dict) + if "parameters" in result and "qubits" in result["parameters"]: + assert "metadata" not in result["parameters"]["qubits"] + + def test_serialize_graph_with_complex_connectivity( + self, mocker, mock_machine, mock_library + ): + """Test serialization with complex graph connectivity""" + mocker.patch.object(QualibrationNode, "_get_storage_manager") + + nodes = [] + for i in range(4): + node = QualibrationNode( + name=f"node{i}", + parameters=NodeParametersWithQubits(qubits=["q1"]), + machine=mock_machine + ) + nodes.append(node) + + with QualibrationGraph.build( + "test_graph", + parameters=GraphParametersWithQubits(qubits=["q1"]) + ) as graph: + for node in nodes: + graph.add_node(node) + + # Create diamond pattern + graph.connect("node0", "node1") + graph.connect("node0", "node2") + graph.connect("node1", "node3") + graph.connect("node2", "node3") + + result = graph.serialize() + + # Should still have metadata regardless of connectivity pattern + assert "metadata" in result["parameters"]["qubits"] \ No newline at end of file From 91b2b75a7250d09de6a815f1fa6cabedddd1c463 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Wed, 31 Dec 2025 16:20:53 +0200 Subject: [PATCH 67/73] finished graph serialization tests --- qualibrate/qualibration_graph.py | 37 ++++++++++++------ tests/unit/test_qualibration_graph/.DS_Store | Bin 0 -> 6148 bytes ..._qualibration_graph_qubit_serialization.py | 29 ++++++++------ 3 files changed, 41 insertions(+), 25 deletions(-) create mode 100644 tests/unit/test_qualibration_graph/.DS_Store diff --git a/qualibrate/qualibration_graph.py b/qualibrate/qualibration_graph.py index 613de69..bdecc08 100644 --- a/qualibrate/qualibration_graph.py +++ b/qualibrate/qualibration_graph.py @@ -937,22 +937,33 @@ def _add_loop_to_edge(self, node_name: str) -> dict[str, Any]: } def _get_machine_metadata(self): - # if self._check_machines_metadata_equality(): - if self._elements is None: - logger.warning(f"Graph {self.name} has no elements hence cant extract machine metadata") + if not self._elements: + logger.warning( + f"Graph {self.name} has no elements hence cant extract machine metadata" + ) return None - elements_machines_metadata = (element._get_machine_metadata() for element - in self._elements.values() if element._get_machine_metadata() is not None) - try: - first = next(elements_machines_metadata) - except StopIteration: - logger.info(f"All elements have no quam machine in graph {self.name}") - return None # all None (or empty) - if all(v == first for v in elements_machines_metadata): + + machines_metadata = [] + + for element in self._elements.values(): + metadata = element._get_machine_metadata() + if metadata: + machines_metadata.append(metadata) + + if not machines_metadata: + logger.info( + f"No elements with usable machine metadata in graph {self.name}" + ) + return None + + first = machines_metadata[0] + if all(machine_metadata == first for machine_metadata in machines_metadata[1:]): return first - logger.warning(f"Not all machines have the same metadata hence cant choose one in graph {self.name}") - return None + logger.warning( + f"Not all machines have the same metadata hence cant choose one in graph {self.name}" + ) + return None diff --git a/tests/unit/test_qualibration_graph/.DS_Store b/tests/unit/test_qualibration_graph/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..d4a411f5c80658e15f7e062196d02128cc1cf92a GIT binary patch literal 6148 zcmeHK!A=4(5S;?b5;WnU3CAX0NdyIq@v=eu0oUk34ekojjq8@MiyR2ap7n?P62Hfp zwipR=@n8&@Nv3Z)o!O?Zn@$S=qBHI{04f0Bpc3Xv*!&md~S8v%^KhZI6k zOVRB3j||Y>ZNWY6T74dO9z>u4(Fz%~KM~<qH{en;FO$F zuTq^(8?EM1O}5&znw%aV)N8VN*q+Tw&hFm+$$9T7dXCkrAywf2r)AsX9Ny4axxN=~ z6h|t4z?gadJdcqXUn+a literal 0 HcmV?d00001 diff --git a/tests/unit/test_qualibration_graph/test_qualibration_graph_qubit_serialization.py b/tests/unit/test_qualibration_graph/test_qualibration_graph_qubit_serialization.py index 559144e..9b93451 100644 --- a/tests/unit/test_qualibration_graph/test_qualibration_graph_qubit_serialization.py +++ b/tests/unit/test_qualibration_graph/test_qualibration_graph_qubit_serialization.py @@ -5,7 +5,14 @@ from qualibrate import GraphParameters, NodeParameters, QualibrationGraph, QualibrationNode - +MOCK_QUBITS_FIDELITIES = { + "q1": 0.99, + "q2": 0.95, + "q3": 0.98, + "q4": None, +} + +ACTIVE_QUBITS = {"q1", "q3"} class MockQubit: """Mock qubit class for testing""" @@ -15,7 +22,7 @@ def __init__(self, name: str, gate_fidelity_value: float | None = None): self.gate_fidelity = Mock() self.gate_fidelity.averaged = gate_fidelity_value else: - self.gate_fidelity = Mock(spec=[]) # No 'averaged' attribute + self.gate_fidelity = Mock(spec=[]) class MockMachine: @@ -56,16 +63,14 @@ def mock_library(self, mocker): ) return mock_lib + @pytest.fixture def mock_qubits(self): """Create mock qubits with various configurations""" - qubits = { - "q1": MockQubit("q1", 0.99), - "q2": MockQubit("q2", 0.95), - "q3": MockQubit("q3", 0.98), - "q4": MockQubit("q4", None), # No fidelity + return { + name: MockQubit(name, fidelity) + for name, fidelity in MOCK_QUBITS_FIDELITIES.items() } - return qubits @pytest.fixture def mock_machine(self, mock_qubits): @@ -399,7 +404,7 @@ def test_serialize_graph_with_subgraph( def test_serialize_graph_with_subgraph_no_metadata( self, mocker, node_with_machine, mock_qubits, mock_library ): - """Test serialization when subgraph has no metadata""" + """Subgraph has nodes without machines; parent graph collects valid metadata from nodes that do""" mocker.patch.object(QualibrationNode, "_get_storage_manager") # Create subgraph with node without machine @@ -426,9 +431,9 @@ def test_serialize_graph_with_subgraph_no_metadata( result = parent_graph.serialize() - # Parent graph should not have metadata because subgraph doesn't - if "parameters" in result and "qubits" in result["parameters"]: - assert "metadata" not in result["parameters"]["qubits"] + metadata = result["parameters"]["qubits"]["metadata"] + assert metadata + assert "q1" in metadata class TestGraphSerializationEdgeCases: From 6bac5c63279f7c985696bd3ab3b948bdb6a039b3 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Wed, 31 Dec 2025 16:22:38 +0200 Subject: [PATCH 68/73] linted, still need to finish linting --- qualibrate/q_runnnable.py | 5 +- qualibrate/qualibration_graph.py | 7 +- qualibrate/qualibration_node.py | 12 +- qualibrate/utils/graph_building.py | 16 +-- .../demo_node_with_mocked_machine.py | 22 ++-- .../long_graph.py | 1 + .../long_graph_sub_graph.py | 6 +- ..._qualibration_graph_qubit_serialization.py | 122 ++++++++++-------- .../test_qubit_inside_node_serialization.py | 35 ++--- 9 files changed, 123 insertions(+), 103 deletions(-) diff --git a/qualibrate/q_runnnable.py b/qualibrate/q_runnnable.py index b10667c..6294ce5 100644 --- a/qualibrate/q_runnnable.py +++ b/qualibrate/q_runnnable.py @@ -243,12 +243,9 @@ def run(self, **passed_parameters: Any) -> BaseRunSummary: tuple[QRunnable, BaseRunSummary]: The executed runnable and summary. """ pass + @abstractmethod def _get_machine_metadata(self): - - - - pass @property diff --git a/qualibrate/qualibration_graph.py b/qualibrate/qualibration_graph.py index bdecc08..e064263 100644 --- a/qualibrate/qualibration_graph.py +++ b/qualibrate/qualibration_graph.py @@ -957,7 +957,10 @@ def _get_machine_metadata(self): return None first = machines_metadata[0] - if all(machine_metadata == first for machine_metadata in machines_metadata[1:]): + if all( + machine_metadata == first + for machine_metadata in machines_metadata[1:] + ): return first logger.warning( @@ -965,8 +968,6 @@ def _get_machine_metadata(self): ) return None - - def __serialize_data(self, /, **kwargs: Any) -> Mapping[str, Any]: """ Serializes the graph into a dictionary format. diff --git a/qualibrate/qualibration_node.py b/qualibrate/qualibration_node.py index d8598ce..e5db24d 100644 --- a/qualibrate/qualibration_node.py +++ b/qualibrate/qualibration_node.py @@ -534,9 +534,9 @@ def _extract_machines_metadata(self): metadata = {} try: if ( - self.parameters is not None and - hasattr(self.parameters,"qubits") and - self.machine is not None + self.parameters is not None + and hasattr(self.parameters, "qubits") + and self.machine is not None and hasattr(self.machine, "active_qubits") and hasattr(self.machine, "qubits") ): @@ -590,7 +590,11 @@ def serialize(self, **kwargs: Any) -> Mapping[str, Any]: metadata = self._get_machine_metadata() - if "parameters" in data and "qubits" in data["parameters"] and metadata is not None: + if ( + "parameters" in data + and "qubits" in data["parameters"] + and metadata is not None + ): data["parameters"]["qubits"]["metadata"] = metadata return data diff --git a/qualibrate/utils/graph_building.py b/qualibrate/utils/graph_building.py index 03a9fe4..64bd820 100644 --- a/qualibrate/utils/graph_building.py +++ b/qualibrate/utils/graph_building.py @@ -9,11 +9,12 @@ TypeVar, cast, ) -from qualibrate.utils.logger_m import logger + import networkx as nx from qualibrate.parameters import RunnableParameters from qualibrate.q_runnnable import QRunnable +from qualibrate.utils.logger_m import logger if TYPE_CHECKING: from qualibrate.qualibration_graph import QualibrationGraph @@ -119,20 +120,18 @@ def _get_machine_for_graph(machine): metadata = {} try: if ( - machine is not None - and hasattr(machine, "active_qubits") - and hasattr(machine, "qubits") + machine is not None + and hasattr(machine, "active_qubits") + and hasattr(machine, "qubits") ): qubits = machine.qubits.keys() - active_qubits = { - qubit.name for qubit in machine.active_qubits - } + active_qubits = {qubit.name for qubit in machine.active_qubits} for qubit in qubits: qubit_info = machine.qubits[qubit] gate_fidelity = None if hasattr(qubit_info, "gate_fidelity") and hasattr( - qubit_info.gate_fidelity, "averaged" + qubit_info.gate_fidelity, "averaged" ): gate_fidelity = qubit_info.gate_fidelity.averaged metadata[qubit] = { @@ -148,6 +147,7 @@ def _get_machine_for_graph(machine): ) return {} return metadata + @staticmethod def cytoscape_representation( serialized: Mapping[str, Any], diff --git a/tests/calibration_nodes_and_graphs_for_testing/demo_node_with_mocked_machine.py b/tests/calibration_nodes_and_graphs_for_testing/demo_node_with_mocked_machine.py index b766e7a..c69a707 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/demo_node_with_mocked_machine.py +++ b/tests/calibration_nodes_and_graphs_for_testing/demo_node_with_mocked_machine.py @@ -2,28 +2,31 @@ from qualibrate import NodeParameters, QualibrationNode + class FakeGateFidelity: def __init__(self, averaged: float): self.averaged = averaged + class FakeQubit: - def __init__(self, name: str, gate_fidelity: FakeGateFidelity | None = None): + def __init__( + self, name: str, gate_fidelity: FakeGateFidelity | None = None + ): self.name = name self.gate_fidelity = gate_fidelity + class FakeMachine: def __init__(self): # qubits dict self.qubits = { "q1": FakeQubit("q1", FakeGateFidelity(0.99)), "q2": FakeQubit("q2", FakeGateFidelity(0.95)), - "q3": FakeQubit("q3") # no gate fidelity + "q3": FakeQubit("q3"), # no gate fidelity } # active_qubits list - self.active_qubits = [ - self.qubits["q1"], - self.qubits["q3"] - ] + self.active_qubits = [self.qubits["q1"], self.qubits["q3"]] + class Color(Enum): RED = "red" @@ -38,15 +41,16 @@ class Parameters(NodeParameters): float_value: float = 1 random_color: Color = Color.BLUE -node = QualibrationNode("test_node_mocked_machine", parameters=Parameters(), machine=FakeMachine() + +node = QualibrationNode( + "test_node_mocked_machine", parameters=Parameters(), machine=FakeMachine() ) @node.run_action() def node_runs_indication(node: QualibrationNode): - node.log("node is running") node.machine = FakeMachine() result = node.serialize() print(result) - return result \ No newline at end of file + return result diff --git a/tests/calibration_nodes_and_graphs_for_testing/long_graph.py b/tests/calibration_nodes_and_graphs_for_testing/long_graph.py index becbc47..0116d4c 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/long_graph.py +++ b/tests/calibration_nodes_and_graphs_for_testing/long_graph.py @@ -6,6 +6,7 @@ USED_NODE = "test_node_mocked_machine" + class Parameters(GraphParameters): targets_name: ClassVar[str] = "qubits" qubits: list[str] diff --git a/tests/calibration_nodes_and_graphs_for_testing/long_graph_sub_graph.py b/tests/calibration_nodes_and_graphs_for_testing/long_graph_sub_graph.py index db77a62..645b086 100644 --- a/tests/calibration_nodes_and_graphs_for_testing/long_graph_sub_graph.py +++ b/tests/calibration_nodes_and_graphs_for_testing/long_graph_sub_graph.py @@ -1,19 +1,21 @@ from typing import ClassVar + from qualibrate import GraphParameters, QualibrationGraph, QualibrationLibrary library = QualibrationLibrary.get_active_library() USED_NODE = "test_node_mocked_machine" + class Parameters(GraphParameters): targets_name: ClassVar[str] = "qubits" qubits: list[str] + # Build the main graph with QualibrationGraph.build( "long_graph_sub_graph", parameters=Parameters(qubits=[f"q{i}" for i in range(3)]), ) as graph: - # # Add nodes to the main graph graph.add_node(library.nodes.get_nocopy(USED_NODE).copy(name="node_1")) for i in range(2, 5): @@ -43,4 +45,4 @@ class Parameters(GraphParameters): if __name__ == "__main__": # result = graph.run() result = graph.serialize() - print(result) \ No newline at end of file + print(result) diff --git a/tests/unit/test_qualibration_graph/test_qualibration_graph_qubit_serialization.py b/tests/unit/test_qualibration_graph/test_qualibration_graph_qubit_serialization.py index 9b93451..0501d43 100644 --- a/tests/unit/test_qualibration_graph/test_qualibration_graph_qubit_serialization.py +++ b/tests/unit/test_qualibration_graph/test_qualibration_graph_qubit_serialization.py @@ -3,7 +3,12 @@ import pytest -from qualibrate import GraphParameters, NodeParameters, QualibrationGraph, QualibrationNode +from qualibrate import ( + GraphParameters, + NodeParameters, + QualibrationGraph, + QualibrationNode, +) MOCK_QUBITS_FIDELITIES = { "q1": 0.99, @@ -13,6 +18,8 @@ } ACTIVE_QUBITS = {"q1", "q3"} + + class MockQubit: """Mock qubit class for testing""" @@ -29,7 +36,7 @@ class MockMachine: """Mock machine class for testing""" def __init__( - self, qubits_dict: dict[str, Any], active_qubits_list: list[MockQubit] + self, qubits_dict: dict[str, Any], active_qubits_list: list[MockQubit] ): self.qubits = qubits_dict self.active_qubits = active_qubits_list @@ -63,7 +70,6 @@ def mock_library(self, mocker): ) return mock_lib - @pytest.fixture def mock_qubits(self): """Create mock qubits with various configurations""" @@ -85,7 +91,7 @@ def node_with_machine(self, mocker, mock_machine): node = QualibrationNode( name="test_node", parameters=NodeParametersWithQubits(qubits=["q1", "q2", "q3"]), - machine=mock_machine + machine=mock_machine, ) return node @@ -95,7 +101,7 @@ def node_without_machine(self, mocker): mocker.patch.object(QualibrationNode, "_get_storage_manager") node = QualibrationNode( name="test_node", - parameters=NodeParametersWithQubits(qubits=["q1", "q2"]) + parameters=NodeParametersWithQubits(qubits=["q1", "q2"]), ) return node @@ -108,8 +114,8 @@ def simple_graph_with_nodes(self, mocker, node_with_machine, mock_library): node2 = node_with_machine.copy(name="node2") with QualibrationGraph.build( - "test_graph", - parameters=GraphParametersWithQubits(qubits=["q1", "q2", "q3"]) + "test_graph", + parameters=GraphParametersWithQubits(qubits=["q1", "q2", "q3"]), ) as graph: graph.add_node(node1) graph.add_node(node2) @@ -118,7 +124,7 @@ def simple_graph_with_nodes(self, mocker, node_with_machine, mock_library): return graph def test_serialize_graph_without_qubits_in_parameters( - self, mocker, node_with_machine, mock_library + self, mocker, node_with_machine, mock_library ): """Test graph serialization when parameters don't have qubits field""" mocker.patch.object(QualibrationNode, "_get_storage_manager") @@ -126,8 +132,8 @@ def test_serialize_graph_without_qubits_in_parameters( node1 = node_with_machine.copy(name="node1") with QualibrationGraph.build( - "test_graph", - parameters=GraphParameters() # No qubits field + "test_graph", + parameters=GraphParameters(), # No qubits field ) as graph: graph.add_node(node1) @@ -136,11 +142,12 @@ def test_serialize_graph_without_qubits_in_parameters( assert isinstance(result, dict) # Should not have qubits metadata when parameters don't have qubits if "parameters" in result: - assert "qubits" not in result["parameters"] or \ - "metadata" not in result["parameters"].get("qubits", {}) + assert "qubits" not in result[ + "parameters" + ] or "metadata" not in result["parameters"].get("qubits", {}) def test_serialize_graph_with_nodes_having_machines( - self, simple_graph_with_nodes, mock_qubits + self, simple_graph_with_nodes, mock_qubits ): """Test graph serialization when all nodes have machines""" result = simple_graph_with_nodes.serialize() @@ -169,7 +176,7 @@ def test_serialize_graph_with_nodes_having_machines( assert metadata["q4"]["fidelity"] is None def test_serialize_graph_with_node_without_machine( - self, mocker, node_with_machine, node_without_machine, mock_library + self, mocker, node_with_machine, node_without_machine, mock_library ): """Test graph serialization when one node doesn't have machine""" mocker.patch.object(QualibrationNode, "_get_storage_manager") @@ -178,8 +185,8 @@ def test_serialize_graph_with_node_without_machine( node2 = node_without_machine.copy(name="node2") with QualibrationGraph.build( - "test_graph", - parameters=GraphParametersWithQubits(qubits=["q1", "q2"]) + "test_graph", + parameters=GraphParametersWithQubits(qubits=["q1", "q2"]), ) as graph: graph.add_node(node1) graph.add_node(node2) @@ -191,7 +198,7 @@ def test_serialize_graph_with_node_without_machine( assert result["parameters"]["qubits"]["metadata"] def test_serialize_graph_with_different_machines( - self, mocker, mock_qubits, mock_library + self, mocker, mock_qubits, mock_library ): """Test graph serialization when nodes have different machines""" mocker.patch.object(QualibrationNode, "_get_storage_manager") @@ -207,18 +214,18 @@ def test_serialize_graph_with_different_machines( node1 = QualibrationNode( name="node1", parameters=NodeParametersWithQubits(qubits=["q1", "q2"]), - machine=machine1 + machine=machine1, ) node2 = QualibrationNode( name="node2", parameters=NodeParametersWithQubits(qubits=["q1", "q2"]), - machine=machine2 + machine=machine2, ) with QualibrationGraph.build( - "test_graph", - parameters=GraphParametersWithQubits(qubits=["q1", "q2"]) + "test_graph", + parameters=GraphParametersWithQubits(qubits=["q1", "q2"]), ) as graph: graph.add_node(node1) graph.add_node(node2) @@ -231,7 +238,7 @@ def test_serialize_graph_with_different_machines( assert "metadata" not in result["parameters"]["qubits"] def test_serialize_graph_with_single_node( - self, mocker, node_with_machine, mock_library + self, mocker, node_with_machine, mock_library ): """Test graph serialization with single node""" mocker.patch.object(QualibrationNode, "_get_storage_manager") @@ -239,8 +246,8 @@ def test_serialize_graph_with_single_node( node1 = node_with_machine.copy(name="node1") with QualibrationGraph.build( - "test_graph", - parameters=GraphParametersWithQubits(qubits=["q1", "q2"]) + "test_graph", + parameters=GraphParametersWithQubits(qubits=["q1", "q2"]), ) as graph: graph.add_node(node1) @@ -255,8 +262,8 @@ def test_serialize_empty_graph(self, mocker, mock_library): mocker.patch.object(QualibrationNode, "_get_storage_manager") with QualibrationGraph.build( - "test_graph", - parameters=GraphParametersWithQubits(qubits=["q1", "q2"]) + "test_graph", + parameters=GraphParametersWithQubits(qubits=["q1", "q2"]), ) as graph: pass # No nodes added @@ -293,19 +300,18 @@ def test_serialize_graph_preserves_base_data(self, simple_graph_with_nodes): assert "connectivity" in result def test_serialize_graph_with_nodes_no_qubits_attribute( - self, mocker, mock_library + self, mocker, mock_library ): """Test graph serialization when node parameters don't have qubits""" mocker.patch.object(QualibrationNode, "_get_storage_manager") node1 = QualibrationNode( name="node1", - parameters=NodeParameters() # No qubits field + parameters=NodeParameters(), # No qubits field ) with QualibrationGraph.build( - "test_graph", - parameters=GraphParametersWithQubits(qubits=["q1"]) + "test_graph", parameters=GraphParametersWithQubits(qubits=["q1"]) ) as graph: graph.add_node(node1) @@ -353,7 +359,7 @@ def node_with_machine(self, mocker, mock_machine): node = QualibrationNode( name="test_node", parameters=NodeParametersWithQubits(qubits=["q1", "q2"]), - machine=mock_machine + machine=mock_machine, ) return node @@ -366,8 +372,8 @@ def subgraph_with_nodes(self, mocker, node_with_machine, mock_library): node2 = node_with_machine.copy(name="subnode2") with QualibrationGraph.build( - "subgraph", - parameters=GraphParametersWithQubits(qubits=["q1", "q2"]) + "subgraph", + parameters=GraphParametersWithQubits(qubits=["q1", "q2"]), ) as subgraph: subgraph.add_node(node1) subgraph.add_node(node2) @@ -376,7 +382,12 @@ def subgraph_with_nodes(self, mocker, node_with_machine, mock_library): return subgraph def test_serialize_graph_with_subgraph( - self, mocker, node_with_machine, subgraph_with_nodes, mock_qubits, mock_library + self, + mocker, + node_with_machine, + subgraph_with_nodes, + mock_qubits, + mock_library, ): """Test serialization of graph containing subgraph""" mocker.patch.object(QualibrationNode, "_get_storage_manager") @@ -384,8 +395,8 @@ def test_serialize_graph_with_subgraph( node1 = node_with_machine.copy(name="node1") with QualibrationGraph.build( - "parent_graph", - parameters=GraphParametersWithQubits(qubits=["q1", "q2"]) + "parent_graph", + parameters=GraphParametersWithQubits(qubits=["q1", "q2"]), ) as parent_graph: parent_graph.add_node(node1) parent_graph.add_node(subgraph_with_nodes) @@ -402,28 +413,26 @@ def test_serialize_graph_with_subgraph( assert set(metadata.keys()) == set(mock_qubits.keys()) def test_serialize_graph_with_subgraph_no_metadata( - self, mocker, node_with_machine, mock_qubits, mock_library + self, mocker, node_with_machine, mock_qubits, mock_library ): """Subgraph has nodes without machines; parent graph collects valid metadata from nodes that do""" mocker.patch.object(QualibrationNode, "_get_storage_manager") # Create subgraph with node without machine node_no_machine = QualibrationNode( - name="subnode1", - parameters=NodeParametersWithQubits(qubits=["q1"]) + name="subnode1", parameters=NodeParametersWithQubits(qubits=["q1"]) ) with QualibrationGraph.build( - "subgraph", - parameters=GraphParametersWithQubits(qubits=["q1"]) + "subgraph", parameters=GraphParametersWithQubits(qubits=["q1"]) ) as subgraph: subgraph.add_node(node_no_machine) node1 = node_with_machine.copy(name="node1") with QualibrationGraph.build( - "parent_graph", - parameters=GraphParametersWithQubits(qubits=["q1", "q2"]) + "parent_graph", + parameters=GraphParametersWithQubits(qubits=["q1", "q2"]), ) as parent_graph: parent_graph.add_node(node1) parent_graph.add_node(subgraph) @@ -460,7 +469,7 @@ def mock_machine(self): return MockMachine(qubits, [qubits["q1"]]) def test_serialize_graph_with_many_identical_nodes( - self, mocker, mock_machine, mock_library + self, mocker, mock_machine, mock_library ): """Test serialization with many nodes having identical machines""" mocker.patch.object(QualibrationNode, "_get_storage_manager") @@ -471,13 +480,12 @@ def test_serialize_graph_with_many_identical_nodes( node = QualibrationNode( name=f"node{i}", parameters=NodeParametersWithQubits(qubits=["q1"]), - machine=mock_machine + machine=mock_machine, ) nodes.append(node) with QualibrationGraph.build( - "test_graph", - parameters=GraphParametersWithQubits(qubits=["q1"]) + "test_graph", parameters=GraphParametersWithQubits(qubits=["q1"]) ) as graph: for node in nodes: graph.add_node(node) @@ -492,10 +500,12 @@ def test_serialize_graph_with_many_identical_nodes( assert "parameters" in result assert "qubits" in result["parameters"] assert "metadata" in result["parameters"]["qubits"] - assert result["parameters"]["qubits"]["metadata"]["q1"]["fidelity"] == 0.99 + assert ( + result["parameters"]["qubits"]["metadata"]["q1"]["fidelity"] == 0.99 + ) def test_serialize_graph_with_machine_missing_attributes( - self, mocker, mock_library + self, mocker, mock_library ): """Test serialization when machine is missing some attributes""" mocker.patch.object(QualibrationNode, "_get_storage_manager") @@ -507,12 +517,11 @@ def test_serialize_graph_with_machine_missing_attributes( node = QualibrationNode( name="node1", parameters=NodeParametersWithQubits(qubits=["q1"]), - machine=machine + machine=machine, ) with QualibrationGraph.build( - "test_graph", - parameters=GraphParametersWithQubits(qubits=["q1"]) + "test_graph", parameters=GraphParametersWithQubits(qubits=["q1"]) ) as graph: graph.add_node(node) @@ -524,7 +533,7 @@ def test_serialize_graph_with_machine_missing_attributes( assert "metadata" not in result["parameters"]["qubits"] def test_serialize_graph_with_complex_connectivity( - self, mocker, mock_machine, mock_library + self, mocker, mock_machine, mock_library ): """Test serialization with complex graph connectivity""" mocker.patch.object(QualibrationNode, "_get_storage_manager") @@ -534,13 +543,12 @@ def test_serialize_graph_with_complex_connectivity( node = QualibrationNode( name=f"node{i}", parameters=NodeParametersWithQubits(qubits=["q1"]), - machine=mock_machine + machine=mock_machine, ) nodes.append(node) with QualibrationGraph.build( - "test_graph", - parameters=GraphParametersWithQubits(qubits=["q1"]) + "test_graph", parameters=GraphParametersWithQubits(qubits=["q1"]) ) as graph: for node in nodes: graph.add_node(node) @@ -554,4 +562,4 @@ def test_serialize_graph_with_complex_connectivity( result = graph.serialize() # Should still have metadata regardless of connectivity pattern - assert "metadata" in result["parameters"]["qubits"] \ No newline at end of file + assert "metadata" in result["parameters"]["qubits"] diff --git a/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py b/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py index 919d52a..6a10c37 100644 --- a/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py +++ b/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py @@ -53,8 +53,7 @@ def base_node_with_qubits_params(self, mocker): """Create a basic node with qubits in parameters""" mocker.patch.object(QualibrationNode, "_get_storage_manager") node = QualibrationNode( - name="test_node", - parameters=ParametersWithQubits() + name="test_node", parameters=ParametersWithQubits() ) return node @@ -94,7 +93,9 @@ def test_serialize_without_machine(self, base_node): if "parameters" in result and "qubits" in result["parameters"]: assert "metadata" not in result["parameters"]["qubits"] - def test_serialize_with_machine_but_no_qubits_in_params(self, base_node, mock_qubits): + def test_serialize_with_machine_but_no_qubits_in_params( + self, base_node, mock_qubits + ): """Test serialization when machine exists but parameters don't have qubits field""" active_qubits = [mock_qubits["qA1"]] machine = MockMachine(mock_qubits, active_qubits) @@ -105,9 +106,13 @@ def test_serialize_with_machine_but_no_qubits_in_params(self, base_node, mock_qu assert isinstance(result, dict) # Should not have qubits metadata when parameters don't have qubits field if "parameters" in result: - assert "qubits" not in result["parameters"] or "metadata" not in result["parameters"].get("qubits", {}) + assert "qubits" not in result[ + "parameters" + ] or "metadata" not in result["parameters"].get("qubits", {}) - def test_serialize_with_machine_no_attributes(self, base_node_with_qubits_params): + def test_serialize_with_machine_no_attributes( + self, base_node_with_qubits_params + ): """Test serialization when machine lacks required attributes""" base_node_with_qubits_params.machine = Mock( spec=[] @@ -181,7 +186,9 @@ def test_serialize_active_qubit_identification( for qubit in metadata: assert (metadata[qubit]["active"]) == (qubit in active_qubits_names) - def test_serialize_qubit_without_gate_fidelity(self, base_node_with_qubits_params): + def test_serialize_qubit_without_gate_fidelity( + self, base_node_with_qubits_params + ): """Test serialization when qubit doesn't have gate_fidelity attribute""" qubits = { "qA1": Mock(spec=["name"]), # No gate_fidelity attribute @@ -197,7 +204,9 @@ def test_serialize_qubit_without_gate_fidelity(self, base_node_with_qubits_param assert metadata["qA1"]["active"] is False assert metadata["qA1"]["fidelity"] is None - def test_serialize_qubit_without_averaged_fidelity(self, base_node_with_qubits_params): + def test_serialize_qubit_without_averaged_fidelity( + self, base_node_with_qubits_params + ): """Test serialization when gate_fidelity doesn't have 'averaged' attribute""" qubits = { @@ -259,10 +268,7 @@ class TestNodeSerializationEdgeCases: def test_serialize_with_partial_machine_attributes(self, mocker): """Test machine with only some attributes""" mocker.patch.object(QualibrationNode, "_get_storage_manager") - node = QualibrationNode( - name="test", - parameters=ParametersWithQubits() - ) + node = QualibrationNode(name="test", parameters=ParametersWithQubits()) # Machine with only active_qubits node.machine = Mock(spec=["active_qubits"]) @@ -284,10 +290,7 @@ def test_serialize_with_partial_machine_attributes(self, mocker): def test_serialize_large_number_of_qubits(self, mocker): """Test serialization with a large number of qubits""" mocker.patch.object(QualibrationNode, "_get_storage_manager") - node = QualibrationNode( - name="test", - parameters=ParametersWithQubits() - ) + node = QualibrationNode(name="test", parameters=ParametersWithQubits()) # Create 100 qubits qubits = { @@ -309,4 +312,4 @@ def test_serialize_large_number_of_qubits(self, mocker): assert "q0" in active_qubits_names assert "q1" not in active_qubits_names assert "q50" in active_qubits_names - assert "q51" not in active_qubits_names \ No newline at end of file + assert "q51" not in active_qubits_names From b55951fb4e935d15548186eebdcb2337f897e4d0 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Wed, 31 Dec 2025 17:50:31 +0200 Subject: [PATCH 69/73] finishe with cr and linting --- qualibrate/q_runnnable.py | 2 +- qualibrate/qualibration_graph.py | 15 ++++---- qualibrate/qualibration_node.py | 18 +++++----- qualibrate/utils/graph_building.py | 35 ------------------- .../test_qubit_inside_node_serialization.py | 4 +-- 5 files changed, 19 insertions(+), 55 deletions(-) diff --git a/qualibrate/q_runnnable.py b/qualibrate/q_runnnable.py index 6294ce5..760f256 100644 --- a/qualibrate/q_runnnable.py +++ b/qualibrate/q_runnnable.py @@ -245,7 +245,7 @@ def run(self, **passed_parameters: Any) -> BaseRunSummary: pass @abstractmethod - def _get_machine_metadata(self): + def _get_machine_metadata(self) -> Mapping[str, Any] | None: pass @property diff --git a/qualibrate/qualibration_graph.py b/qualibrate/qualibration_graph.py index e064263..1d343d1 100644 --- a/qualibrate/qualibration_graph.py +++ b/qualibrate/qualibration_graph.py @@ -936,22 +936,21 @@ def _add_loop_to_edge(self, node_name: str) -> dict[str, Any]: "data": data, } - def _get_machine_metadata(self): + def _get_machine_metadata(self) -> Mapping[str, Any] | None: if not self._elements: logger.warning( f"Graph {self.name} has no elements hence cant extract machine metadata" ) return None - machines_metadata = [] - - for element in self._elements.values(): - metadata = element._get_machine_metadata() - if metadata: - machines_metadata.append(metadata) + machines_metadata = [ + metadata + for element in self._elements.values() + if (metadata := element._get_machine_metadata()) + ] if not machines_metadata: - logger.info( + logger.debug( f"No elements with usable machine metadata in graph {self.name}" ) return None diff --git a/qualibrate/qualibration_node.py b/qualibrate/qualibration_node.py index e5db24d..5bedda3 100644 --- a/qualibrate/qualibration_node.py +++ b/qualibrate/qualibration_node.py @@ -259,7 +259,9 @@ def machine(self) -> NodeMachineType | None: @machine.setter def machine(self, value: NodeMachineType | None) -> None: self._machine = value - self._machine_metadata = None # invalidate cache + self._machine_metadata: Mapping[str, Any] | None = ( + None # invalidate cache + ) def __copy__(self) -> Self: """ @@ -530,7 +532,7 @@ def load_from_id( build_params_class=isinstance(caller, type), ) - def _extract_machines_metadata(self): + def _extract_machines_metadata(self) -> Mapping[str, Any] | None: metadata = {} try: if ( @@ -566,7 +568,8 @@ def _extract_machines_metadata(self): return None return metadata - def _get_machine_metadata(self): + def _get_machine_metadata(self) -> Mapping[str, Any] | None: + # returns None if we dont have the right attributes in machine else {} if self._machine is None: return None @@ -576,13 +579,10 @@ def _get_machine_metadata(self): self._machine_metadata = self._extract_machines_metadata() except AttributeError: self._machine_metadata = None - - # Only return metadata if machine exists and has qubits attribute - if not hasattr(self._machine, "qubits"): - return None - + # if not hasattr(self._machine, "qubits"): + # return None # Always return dict (even empty) if machine has qubits - return self._machine_metadata or {} + return self._machine_metadata def serialize(self, **kwargs: Any) -> Mapping[str, Any]: # Get base QRunnable serialization diff --git a/qualibrate/utils/graph_building.py b/qualibrate/utils/graph_building.py index 64bd820..7b91ee0 100644 --- a/qualibrate/utils/graph_building.py +++ b/qualibrate/utils/graph_building.py @@ -14,7 +14,6 @@ from qualibrate.parameters import RunnableParameters from qualibrate.q_runnnable import QRunnable -from qualibrate.utils.logger_m import logger if TYPE_CHECKING: from qualibrate.qualibration_graph import QualibrationGraph @@ -114,40 +113,6 @@ def nx_graph_export( adj["id"] = adj["id"].name return data - @staticmethod - def _get_machine_for_graph(machine): - # Build metadata for each qubit - metadata = {} - try: - if ( - machine is not None - and hasattr(machine, "active_qubits") - and hasattr(machine, "qubits") - ): - qubits = machine.qubits.keys() - active_qubits = {qubit.name for qubit in machine.active_qubits} - - for qubit in qubits: - qubit_info = machine.qubits[qubit] - gate_fidelity = None - if hasattr(qubit_info, "gate_fidelity") and hasattr( - qubit_info.gate_fidelity, "averaged" - ): - gate_fidelity = qubit_info.gate_fidelity.averaged - metadata[qubit] = { - "active": qubit in active_qubits, - "fidelity": gate_fidelity, - } - - except Exception: - # for any exception that could happen, return the old data as it was - logger.info( - "Failed to serialize quam machine for graph, probably used a different" - " quam package hence the machine json load isn't compatible" - ) - return {} - return metadata - @staticmethod def cytoscape_representation( serialized: Mapping[str, Any], diff --git a/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py b/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py index 6a10c37..f97a4c4 100644 --- a/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py +++ b/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py @@ -121,9 +121,9 @@ def test_serialize_with_machine_no_attributes( result = base_node_with_qubits_params.serialize() assert isinstance(result, dict) - # Should not have qubits metadata when machine lacks attributes + # Should have empty qubits metadata when machine lacks attributes if "parameters" in result and "qubits" in result["parameters"]: - assert "metadata" not in result["parameters"]["qubits"] + assert "metadata" in result["parameters"]["qubits"] def test_serialize_with_complete_machine( self, node_with_machine, mock_qubits From 9c8a9ea128854c6ed4f01ea8e128a3bfadc10289 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Wed, 31 Dec 2025 17:53:12 +0200 Subject: [PATCH 70/73] removed commented code --- qualibrate/qualibration_node.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/qualibrate/qualibration_node.py b/qualibrate/qualibration_node.py index 5bedda3..d7a9ba1 100644 --- a/qualibrate/qualibration_node.py +++ b/qualibrate/qualibration_node.py @@ -569,7 +569,6 @@ def _extract_machines_metadata(self) -> Mapping[str, Any] | None: return metadata def _get_machine_metadata(self) -> Mapping[str, Any] | None: - # returns None if we dont have the right attributes in machine else {} if self._machine is None: return None @@ -579,8 +578,7 @@ def _get_machine_metadata(self) -> Mapping[str, Any] | None: self._machine_metadata = self._extract_machines_metadata() except AttributeError: self._machine_metadata = None - # if not hasattr(self._machine, "qubits"): - # return None + # Always return dict (even empty) if machine has qubits return self._machine_metadata From f506b4fefea17dc6de3b5d8a3e241897618032b3 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Thu, 1 Jan 2026 11:51:13 +0200 Subject: [PATCH 71/73] fixed {} and None logic --- qualibrate/qualibration_node.py | 16 +++++++++------- .../test_qubit_inside_node_serialization.py | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/qualibrate/qualibration_node.py b/qualibrate/qualibration_node.py index d7a9ba1..9ce39b1 100644 --- a/qualibrate/qualibration_node.py +++ b/qualibrate/qualibration_node.py @@ -533,15 +533,17 @@ def load_from_id( ) def _extract_machines_metadata(self) -> Mapping[str, Any] | None: - metadata = {} + metadata = None try: if ( - self.parameters is not None - and hasattr(self.parameters, "qubits") - and self.machine is not None - and hasattr(self.machine, "active_qubits") - and hasattr(self.machine, "qubits") + self.parameters is not None + and hasattr(self.parameters, "qubits") + and + self.machine is not None + and hasattr(self.machine, "active_qubits") + and hasattr(self.machine, "qubits") ): + metadata = {} qubits = self.machine.qubits.keys() active_qubits = { qubit.name for qubit in self.machine.active_qubits @@ -552,7 +554,7 @@ def _extract_machines_metadata(self) -> Mapping[str, Any] | None: qubit_info = self.machine.qubits[qubit] gate_fidelity = None if hasattr(qubit_info, "gate_fidelity") and hasattr( - qubit_info.gate_fidelity, "averaged" + qubit_info.gate_fidelity, "averaged" ): gate_fidelity = qubit_info.gate_fidelity.averaged metadata[qubit] = { diff --git a/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py b/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py index f97a4c4..a2ad989 100644 --- a/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py +++ b/tests/unit/test_qualibration_node/test_qubit_inside_node_serialization.py @@ -123,7 +123,7 @@ def test_serialize_with_machine_no_attributes( assert isinstance(result, dict) # Should have empty qubits metadata when machine lacks attributes if "parameters" in result and "qubits" in result["parameters"]: - assert "metadata" in result["parameters"]["qubits"] + assert "metadata" not in result["parameters"]["qubits"] def test_serialize_with_complete_machine( self, node_with_machine, mock_qubits From 75aed71185a832e19b58b5b0dc041c99d57a1317 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Thu, 1 Jan 2026 13:31:26 +0200 Subject: [PATCH 72/73] linted --- qualibrate/qualibration_node.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/qualibrate/qualibration_node.py b/qualibrate/qualibration_node.py index 9ce39b1..2be93f3 100644 --- a/qualibrate/qualibration_node.py +++ b/qualibrate/qualibration_node.py @@ -536,12 +536,11 @@ def _extract_machines_metadata(self) -> Mapping[str, Any] | None: metadata = None try: if ( - self.parameters is not None - and hasattr(self.parameters, "qubits") - and - self.machine is not None - and hasattr(self.machine, "active_qubits") - and hasattr(self.machine, "qubits") + self.parameters is not None + and hasattr(self.parameters, "qubits") + and self.machine is not None + and hasattr(self.machine, "active_qubits") + and hasattr(self.machine, "qubits") ): metadata = {} qubits = self.machine.qubits.keys() @@ -554,7 +553,7 @@ def _extract_machines_metadata(self) -> Mapping[str, Any] | None: qubit_info = self.machine.qubits[qubit] gate_fidelity = None if hasattr(qubit_info, "gate_fidelity") and hasattr( - qubit_info.gate_fidelity, "averaged" + qubit_info.gate_fidelity, "averaged" ): gate_fidelity = qubit_info.gate_fidelity.averaged metadata[qubit] = { From 25a0f8222f82bfceb7dc3b1a22dba3b8696be3a8 Mon Sep 17 00:00:00 2001 From: EladZ-mac-laptop Date: Thu, 1 Jan 2026 13:57:01 +0200 Subject: [PATCH 73/73] version release 0.6.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 27405d4..3a5bdbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "qualibrate-core" -version = "0.5.0" +version = "0.6.0" description = "" authors = ["Quantum Machines "] readme = "README.md"