diff --git a/notebooks/workflow_example.ipynb b/notebooks/workflow_example.ipynb index defccc32..def5e0a6 100644 --- a/notebooks/workflow_example.ipynb +++ b/notebooks/workflow_example.ipynb @@ -312,7 +312,7 @@ } ], "source": [ - "adder_node(10, y=20)\n", + "adder_node = Function(adder, 10, y=20)\n", "adder_node.run()" ] }, @@ -447,15 +447,17 @@ "source": [ "# Connecting nodes and controlling flow\n", "\n", - "Multiple nodes can be used together to build a computational graph, with each node performing a particular operation in the overall workflow:\n", + "Multiple nodes can be used together to build a computational graph, with each node performing a particular operation in the overall workflow.\n", "\n", - "The input and output of nodes can be chained together by connecting their data channels. When a node runs, its output channels will push their new value to each input node to whom they are connected. In this way, data propagates forwards\n", + "The input and output of nodes can be chained together by connecting their data channels.\n", "\n", - "In addition to input and output data channels, nodes also have \"signal\" channels available. Input signals are bound to a callback function (typically one of its node's methods), and output signals trigger the callbacks for all the input signal channels they're connected to.\n", + "The flow of execution can be manually configured by using other \"signal\" channels. However, for acyclic graphs (DAGs), execution flow can be automatically determined from the topology of the data connections.\n", "\n", - "Standard nodes have a `run` input signal (which is, unsurprisingly, bound to the `run` method), and a `ran` output signal (which, again, hopefully with no great surprise, is triggered at the end of the `run` method.)\n", + "The `run` command we saw above has several boolean flags for controlling the style of execution. The two main run modes are with a \"pull\" paradigm, where everything upstream is run first then the node invoking `pull` gets run; and with a \"push\" paradigm (the default for `run`), where the node invoking `run` gets run and then runs everything downstream. Calling an instantiated node runs a particularly aggressive version of `pull`.\n", "\n", - "In the example below we see how this works for a super-simple toy graph:" + "We'll talk more about grouping nodes together inside a `Workflow` object, but without a parent workflow, only the `pull` method will automate execution signals; trying to push data downstream using `run` requires specifying the execution flow manually.\n", + "\n", + "Let's start by looking at `pull` in the example below to see how this works for a super-simple toy graph:" ] }, { @@ -467,11 +469,24 @@ }, "outputs": [ { - "name": "stdout", + "name": "stderr", "output_type": "stream", "text": [ - "1 2\n" + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:158: UserWarning: The channel ran was not connected to run, andthus could not disconnect from it.\n", + " warn(\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:158: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + " warn(\n" ] + }, + { + "data": { + "text/plain": [ + "2" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ @@ -490,8 +505,37 @@ "t2.inputs.x = l.outputs.x\n", "t2.signals.input.run = l.signals.output.ran\n", "\n", - "l.run()\n", - "print(t2.inputs.x, t2.outputs.double)" + "t2.pull()" + ] + }, + { + "cell_type": "markdown", + "id": "09623591-bbbb-462c-b490-f1db02c9f459", + "metadata": {}, + "source": [ + "And, as mentioned, `__call__` is just (roughly) an alias for `pull`:" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "f3b0b700-683e-43cb-b374-48735e413bc9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "4" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "l.inputs.x = 2\n", + "t2()" ] }, { @@ -499,18 +543,18 @@ "id": "5da1ecfc-7145-4fb2-b5c0-417f050c5de4", "metadata": {}, "source": [ - "We can use a couple pieces of syntactic sugar to make this faster.\n", + "Next, lets see how to do this with a \"push\" paradigm.\n", "\n", - "First: data connections can be made with keyword arguments just like other input data definitions.\n", + "Just like the data connections, we can connect the `.signals.inputs.run` and `.signals.output.ran` channels of two nodes, but we can also use the `>` operator as a syntactic sugar shortcut.\n", "\n", - "Second: the `>` is a shortcut for creating connections between the left-hand node's `signals.output.ran` channel and the right-hand node's `signals.input.run` channel.\n", + "Note how data connections can be made with keyword arguments just like other input data definitions.\n", "\n", "With both of these together, we can write:" ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 19, "id": "59c29856-c77e-48a1-9f17-15d4c58be588", "metadata": {}, "outputs": [ @@ -525,7 +569,7 @@ "source": [ "l = linear(x=10)\n", "t2 = times_two(x=l.outputs.x)\n", - "l > t2\n", + "l > t2 # Note: We can make arbitrarily long linear chains: l > t2 > something_else > another_node\n", "l.run()\n", "print(t2.inputs.x, t2.outputs.double)" ] @@ -546,7 +590,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 20, "id": "1a4e9693-0980-4435-aecc-3331d8b608dd", "metadata": {}, "outputs": [], @@ -558,7 +602,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 21, "id": "7c4d314b-33bb-4a67-bfb9-ed77fba3949c", "metadata": {}, "outputs": [ @@ -580,7 +624,7 @@ " return linspace\n", "\n", "lin = SingleValue(linspace_node)\n", - "lin.run()\n", + "lin()\n", "\n", "print(type(lin.outputs.linspace.value)) # Output is just what we expect\n", "print(lin[1:4]) # Gets items from the output\n", @@ -597,16 +641,19 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 22, "id": "61ae572f-197b-4a60-8d3e-e19c1b9cc6e2", "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "times_two (TimesTwo) output single-value: 4\n" - ] + "data": { + "text/plain": [ + "4" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ @@ -622,9 +669,7 @@ "\n", "l = linear(x=2)\n", "t2 = times_two(x=l) # Just takes the whole `l` node!\n", - "l > t2\n", - "l.run()\n", - "print(t2)" + "t2.pull()" ] }, { @@ -632,29 +677,26 @@ "id": "b2e56a64-d053-4127-bb8c-069777c1c6b5", "metadata": {}, "source": [ - "Nodes can take input from multiple sources, and we can chain together these execution orders:" + "Nodes can take input from multiple sources, and -- although it's usually _useful_ to give each node its own variable -- we can even instantiate nodes inside the signature for initializing another node and call that node all at once! You won't have easy access to them, but this still just builds three nodes in memory, sets their data connections, and invokes a `pull` on the outermost (downstream-most) node, which automatically creates the execution flow and runs it:" ] }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 23, "id": "6569014a-815b-46dd-8b47-4e1cd4584b3b", "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "array([0.49455794, 0.6789772 , 0.48470916, 0.43574953, 0.18030331,\n", - " 0.6059215 , 0.65871187, 0.42205006, 0.65062977, 0.5390317 ])" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:158: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + " warn(\n" + ] }, { "data": { - "image/png": "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", + "image/png": "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ssrKyoL2/+c//6lrr71Wa9asUUpKin75y1/qoYce0rFjx664eAAA4Hy2wsj58+dVU1Oj3NzcgPbc3FwdPnw46JicnBydOXNGFRUVsixLX375pfbs2aM777wz9KoBAEDYsBVGWltb1d3drcTExID2xMRENTc3Bx2Tk5OjXbt2KT8/X7GxsUpKStJVV12ll19+ecDX6erqUnt7e8ADAACEp5AuYHW5XAHPLcvq19arrq5Oa9as0VNPPaWamhpVVlaqoaFBBQUFA/7+kpISeTyevkdycnIoZQIAAAdwWZZlDbbz+fPnNW7cOO3evVt33313X/vatWt18uRJHThwoN+YZcuWqbOzU7t37+5rO3jwoObNm6ezZ8/K6/X2G9PV1aWurq6+5+3t7UpOTpbf71d8fPyg3xwAADCnvb1dHo/nsp/ftlZGYmNjlZGRoaqqqoD2qqoq5eTkBB3z7bffKioq8GWio6MlXVxRCSYuLk7x8fEBDwAAEJ5sb9MUFhbqjTfe0Pbt21VfX6/169ersbGxb9tl48aNWr58eV//vLw8vfvuuyorK9Pp06d16NAhrVmzRllZWZo6derQvRMAAOBIMXYH5Ofnq62tTZs3b5bP51NaWpoqKio0ffp0SZLP5ws4c+T+++9XR0eH/vznP+v3v/+9rrrqKt1222169tlnh+5dAAAAx7J1zYgpg91zAgAAo8ewXDMCAAAw1AgjAADAKMIIAAAwijACAACMIowAAACjCCMAAMAowggAADCKMAIAAIwijAAAAKMIIwAAwCjCCAAAMIowAgAAjCKMAAAAowgjAADAKMIIAAAwijACAACMIowAAACjCCMAAMAowggAADCKMAIAAIwijAAAAKMIIwAAwCjCCAAAMIowAgAAjCKMAAAAowgjAADAKMIIAAAwijACAACMIowAAACjCCMAAMAowggAADCKMAIAAIwijAAAAKMIIwAAwCjCCAAAMIowAgAAjCKMAAAAowgjAADAKMIIAAAwijACAACMIowAAACjCCMAAMAowggAADCKMAIAAIwijAAAAKMIIwAAwCjCCAAAMIowAgAAjCKMAAAAowgjAADAKMIIAAAwijACAACMIowAAACjCCMAAMAowggAADCKMAIAAIwijAAAAKMIIwAAwCjCCAAAMIowAgAAjCKMAAAAo0IKI6WlpUpJSZHb7VZGRoaq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UGjztHfvXmVmZuq5557T1Vdfreuvv16PPfaYvvvuu5EoOeKEMkc5OTk6c+aMKioqZFmWvvzyS+3Zs0d33nnnSJQ8qjnii/JaW1vV3d2txMTEgPbExEQ1NzcHHdPc3By0/4ULF9Ta2iqv1zts9UaqUObpx/70pz/pm2++0ZIlS4ajxIgXyhx99tln2rBhg6qrqxUT44h/MhwvlHk6ffq0Dh48KLfbrffee0+tra16+OGHde7cOa4bGQahzFFOTo527dql/Px8dXZ26sKFC7rrrrv08ssvj0TJo5ojVkZ6uVyugOeWZfVru1z/YO0YWnbnqdc777yjp59+WuXl5ZoyZcpwlQcNfo66u7t13333qbi4WNdff/1IlYf/svO31NPTI5fLpV27dikrK0uLFi3SCy+8oB07drA6MozszFFdXZ3WrFmjp556SjU1NaqsrFRDQ4MKCgpGotRRzRH/zZk0aZKio6P7pc2WlpZ+qbRXUlJS0P4xMTGaOHHisNUayUKZp17l5eVatWqVdu/erdtvv304y4xodueoo6NDx44d04kTJ/Too49KuvihZ1mWYmJitH//ft12220jUnskCeVvyev16uqrrw74uvZZs2bJsiydOXNG11133bDWHGlCmaOSkhLNnTtXjz/+uCTp5z//ucaPH6958+bpmWeeiegVe0esjMTGxiojI0NVVVUB7VVVVcrJyQk6Jjs7u1///fv3KzMzU2PGjBm2WiNZKPMkXVwRuf/++/X222+zdzrM7M5RfHy8Pv74Y508ebLvUVBQoJ/97Gc6efKk5syZM1KlR5RQ/pbmzp2rs2fP6uuvv+5r+/TTTxUVFaVp06YNa72RKJQ5+vbbbxUVFfixGx0dLen/Vu4jlqkrZ+3qvYVq27ZtVl1dnbVu3Tpr/Pjx1n/+8x/Lsixrw4YN1rJly/r6997au379equurs7atm0bt/aOALvz9Pbbb1sxMTHW1q1bLZ/P1/f46quvTL2FsGd3jn6Mu2lGht156ujosKZNm2b9+te/tj755BPrwIED1nXXXWc98MADpt5C2LM7R2+++aYVExNjlZaWWqdOnbIOHjxoZWZmWllZWabewqjhmDBiWZa1detWa/r06VZsbKyVnp5uHThwoO9nK1assG655ZaA/v/4xz+sX/ziF1ZsbKx17bXXWmVlZSNccWSyM0+33HKLJanfY8WKFSNfeASx+7f0Q4SRkWN3nurr663bb7/dGjt2rDVt2jSrsLDQ+vbbb0e46shid462bNlipaamWmPHjrW8Xq/1m9/8xjpz5swIVz36uCwr0teGAACASY64ZgQAAIQvwggAADCKMAIAAIwijAAAAKMIIwAAwCjCCAAAMIowAgAAjCKMAAAAowgjAADAKMIIAAAwijACAACMIowAAACj/j+E4OcogDUlLwAAAABJRU5ErkJggg==", "text/plain": [ "
" ] @@ -676,11 +718,10 @@ " fig = plt.scatter(x, y)\n", " return fig\n", "\n", - "x = noise(length=10)\n", - "y = noise(length=10)\n", - "f = plot(x=x, y=y)\n", - "x > y > f\n", - "x()" + "plot_output = plot(\n", + " x=noise(length=10),\n", + " y=noise(length=10),\n", + ")()" ] }, { @@ -694,7 +735,7 @@ "We offer a formal way to group these objects together as a `Workflow(Node)` object.\n", "`Workflow` also offers us a single point of entry to the codebase -- i.e. most of the time you shouldn't need the node imports used above, because the decorators are available right on the workflow class.\n", "\n", - "We will also see here that we can our node output channels using the `output_labels: Optional[str | list[str] | tuple[str]` kwarg, in case they don't have a convenient name to start with.\n", + "We will also see here that we can rename our node output channels using the `output_labels: Optional[str | list[str] | tuple[str]` kwarg, in case they don't have a convenient name to start with.\n", "This way we can always have convenient dot-based access (and tab completion) instead of having to access things by string-based keys.\n", "\n", "Finally, when a workflow is run, unless its `automate_execution` flag has been set to `False` or the data connections form a cyclic graph, it will _automatically_ build the necessary run signals! That means for all directed acyclic graph (DAG) workflows, all we typically need to worry about is the data connections." @@ -710,7 +751,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 24, "id": "1cd000bd-9b24-4c39-9cac-70a3291d0660", "metadata": {}, "outputs": [], @@ -737,7 +778,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 25, "id": "7964df3c-55af-4c25-afc5-9e07accb606a", "metadata": {}, "outputs": [ @@ -778,7 +819,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 26, "id": "809178a5-2e6b-471d-89ef-0797db47c5ad", "metadata": {}, "outputs": [ @@ -832,7 +873,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 27, "id": "52c48d19-10a2-4c48-ae81-eceea4129a60", "metadata": {}, "outputs": [ @@ -842,7 +883,7 @@ "{'ay': 3, 'a + b + 2': 7}" ] }, - "execution_count": 26, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } @@ -852,6 +893,14 @@ "out" ] }, + { + "cell_type": "markdown", + "id": "a229a66b-54f0-4d79-a16f-669c5f755587", + "metadata": {}, + "source": [ + "Note: Workflows are the \"parent-most\" node, so even though `__call__` is still invoking a `pull`, the \"run all upstream data dependencies\" part of \"run all upstream data dependencies then run yourself\" gets skipped trivially -- workflows can't have siblings or parents so there are no dependencies to run! Thus `__call__` is effectively just a `run`." + ] + }, { "cell_type": "markdown", "id": "e3f4b51b-7c28-47f7-9822-b4755e12bd4d", @@ -862,7 +911,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 28, "id": "bb35ba3e-602d-4c9c-b046-32da9401dd1c", "metadata": {}, "outputs": [ @@ -872,7 +921,7 @@ "(7, 3)" ] }, - "execution_count": 27, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } @@ -891,7 +940,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 29, "id": "2b0d2c85-9049-417b-8739-8a8432a1efbe", "metadata": {}, "outputs": [ @@ -1209,10 +1258,10 @@ "\n" ], "text/plain": [ - "" + "" ] }, - "execution_count": 28, + "execution_count": 29, "metadata": {}, "output_type": "execute_result" } @@ -1239,14 +1288,14 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 30, "id": "ae500d5e-e55b-432c-8b5f-d5892193cdf5", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "4f07c83c4a694b76847e9060c58c00d0", + "model_id": "c46776a009974c03934aeea1cb8be1ce", "version_major": 2, "version_minor": 0 }, @@ -1265,10 +1314,10 @@ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 29, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" }, @@ -1311,7 +1360,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 31, "id": "2114d0c3-cdad-43c7-9ffa-50c36d56d18f", "metadata": {}, "outputs": [ @@ -1330,27 +1379,27 @@ "clusterwith_prebuilt\n", "\n", "with_prebuilt: Workflow\n", - "\n", - "clusterwith_prebuiltOutputs\n", + "\n", + "clusterwith_prebuiltInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Outputs\n", + "\n", + "Inputs\n", "\n", - "\n", - "clusterwith_prebuiltInputs\n", + "\n", + "clusterwith_prebuiltOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Outputs\n", "\n", "\n", "\n", @@ -1519,10 +1568,10 @@ "\n" ], "text/plain": [ - "" + "" ] }, - "execution_count": 30, + "execution_count": 31, "metadata": {}, "output_type": "execute_result" } @@ -1551,7 +1600,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 32, "id": "c71a8308-f8a1-4041-bea0-1c841e072a6d", "metadata": {}, "outputs": [], @@ -1561,7 +1610,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 33, "id": "2b9bb21a-73cd-444e-84a9-100e202aa422", "metadata": {}, "outputs": [ @@ -1579,7 +1628,7 @@ "13" ] }, - "execution_count": 32, + "execution_count": 33, "metadata": {}, "output_type": "execute_result" } @@ -1618,7 +1667,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 34, "id": "3668f9a9-adca-48a4-84ea-13add965897c", "metadata": {}, "outputs": [ @@ -1628,7 +1677,7 @@ "{'intermediate': 102, 'plus_three': 103}" ] }, - "execution_count": 33, + "execution_count": 34, "metadata": {}, "output_type": "execute_result" } @@ -1666,7 +1715,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 35, "id": "9aaeeec0-5f88-4c94-a6cc-45b56d2f0111", "metadata": {}, "outputs": [], @@ -1696,7 +1745,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 36, "id": "a832e552-b3cc-411a-a258-ef21574fc439", "metadata": {}, "outputs": [], @@ -1723,7 +1772,7 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 37, "id": "b764a447-236f-4cb7-952a-7cba4855087d", "metadata": {}, "outputs": [ @@ -1742,159 +1791,159 @@ "clusterphase_preference\n", "\n", "phase_preference: Workflow\n", - "\n", - "clusterphase_preferenceInputs\n", + "\n", + "clusterphase_preferencemin_phase2\n", "\n", - "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "min_phase2: LammpsMinimize\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Inputs\n", + "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", - "\n", - "clusterphase_preferenceOutputs\n", + "\n", + "clusterphase_preferencemin_phase2Outputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Outputs\n", + "\n", + "Outputs\n", "\n", - "\n", - "clusterphase_preferenceelement\n", + "\n", + "clusterphase_preferencecompare\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "element: UserInput\n", + "\n", + "compare: PerAtomEnergyDifference\n", "\n", - "\n", - "clusterphase_preferenceelementInputs\n", + "\n", + "clusterphase_preferencecompareInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", - "\n", - "clusterphase_preferenceelementOutputs\n", + "\n", + "clusterphase_preferencecompareOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Outputs\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "min_phase1: LammpsMinimize\n", + "\n", + "Outputs\n", "\n", - "\n", - "clusterphase_preferencemin_phase1Inputs\n", + "\n", + "clusterphase_preferenceInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", - "\n", - "clusterphase_preferencemin_phase1Outputs\n", + "\n", + "clusterphase_preferenceOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Outputs\n", + "\n", + "Outputs\n", "\n", - "\n", - "clusterphase_preferencemin_phase2\n", + "\n", + "clusterphase_preferenceelement\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", - "\n", - "min_phase2: LammpsMinimize\n", + "\n", + "element: UserInput\n", "\n", - "\n", - "clusterphase_preferencemin_phase2Inputs\n", + "\n", + "clusterphase_preferenceelementInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", - "\n", - "clusterphase_preferencemin_phase2Outputs\n", + "\n", + "clusterphase_preferenceelementOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Outputs\n", + "\n", + "Outputs\n", "\n", - "\n", - "clusterphase_preferencecompare\n", + "\n", + "clusterphase_preferencemin_phase1\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", - "\n", - "compare: PerAtomEnergyDifference\n", + "\n", + "min_phase1: LammpsMinimize\n", "\n", - "\n", - "clusterphase_preferencecompareInputs\n", + "\n", + "clusterphase_preferencemin_phase1Inputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", - "\n", - "clusterphase_preferencecompareOutputs\n", + "\n", + "clusterphase_preferencemin_phase1Outputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Outputs\n", + "\n", + "Outputs\n", "\n", "\n", "\n", @@ -2121,192 +2170,192 @@ "\n", "\n", "clusterphase_preferenceInputsphase2\n", - "\n", - "phase2\n", + "\n", + "phase2\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Inputscrystalstructure\n", - "\n", - "crystalstructure\n", + "\n", + "crystalstructure\n", "\n", "\n", "\n", "clusterphase_preferenceInputsphase2->clusterphase_preferencemin_phase2Inputscrystalstructure\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferenceInputslattice_guess2\n", - "\n", - "lattice_guess2\n", + "\n", + "lattice_guess2\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Inputslattice_guess\n", - "\n", - "lattice_guess\n", + "\n", + "lattice_guess\n", "\n", "\n", "\n", "clusterphase_preferenceInputslattice_guess2->clusterphase_preferencemin_phase2Inputslattice_guess\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__structure__c\n", - "\n", - "min_phase2__structure__c\n", + "\n", + "min_phase2__structure__c\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Inputsstructure__c\n", - "\n", - "structure__c\n", + "\n", + "structure__c\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__structure__c->clusterphase_preferencemin_phase2Inputsstructure__c\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__structure__covera\n", - "\n", - "min_phase2__structure__covera\n", + "\n", + "min_phase2__structure__covera\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Inputsstructure__covera\n", - "\n", - "structure__covera\n", + "\n", + "structure__covera\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__structure__covera->clusterphase_preferencemin_phase2Inputsstructure__covera\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__structure__u\n", - "\n", - "min_phase2__structure__u\n", + "\n", + "min_phase2__structure__u\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Inputsstructure__u\n", - "\n", - "structure__u\n", + "\n", + "structure__u\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__structure__u->clusterphase_preferencemin_phase2Inputsstructure__u\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__structure__orthorhombic\n", - "\n", - "min_phase2__structure__orthorhombic\n", + "\n", + "min_phase2__structure__orthorhombic\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Inputsstructure__orthorhombic\n", - "\n", - "structure__orthorhombic\n", + "\n", + "structure__orthorhombic\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__structure__orthorhombic->clusterphase_preferencemin_phase2Inputsstructure__orthorhombic\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__structure__cubic\n", - "\n", - "min_phase2__structure__cubic\n", + "\n", + "min_phase2__structure__cubic\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Inputsstructure__cubic\n", - "\n", - "structure__cubic\n", + "\n", + "structure__cubic\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__structure__cubic->clusterphase_preferencemin_phase2Inputsstructure__cubic\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__calc__n_ionic_steps\n", - "\n", - "min_phase2__calc__n_ionic_steps: int\n", + "\n", + "min_phase2__calc__n_ionic_steps: int\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Inputscalc__n_ionic_steps\n", - "\n", - "calc__n_ionic_steps: int\n", + "\n", + "calc__n_ionic_steps: int\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__calc__n_ionic_steps->clusterphase_preferencemin_phase2Inputscalc__n_ionic_steps\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__calc__n_print\n", - "\n", - "min_phase2__calc__n_print: int\n", + "\n", + "min_phase2__calc__n_print: int\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Inputscalc__n_print\n", - "\n", - "calc__n_print: int\n", + "\n", + "calc__n_print: int\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__calc__n_print->clusterphase_preferencemin_phase2Inputscalc__n_print\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__calc__pressure\n", - "\n", - "min_phase2__calc__pressure\n", + "\n", + "min_phase2__calc__pressure\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Inputscalc__pressure\n", - "\n", - "calc__pressure\n", + "\n", + "calc__pressure\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__calc__pressure->clusterphase_preferencemin_phase2Inputscalc__pressure\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -2383,74 +2432,74 @@ "\n", "\n", "clusterphase_preferenceOutputsmin_phase2__calc__cells\n", - "\n", - "min_phase2__calc__cells\n", + "\n", + "min_phase2__calc__cells\n", "\n", "\n", "\n", "clusterphase_preferenceOutputsmin_phase2__calc__displacements\n", - "\n", - "min_phase2__calc__displacements\n", + "\n", + "min_phase2__calc__displacements\n", "\n", "\n", "\n", "clusterphase_preferenceOutputsmin_phase2__calc__energy_tot\n", - "\n", - "min_phase2__calc__energy_tot\n", + "\n", + "min_phase2__calc__energy_tot\n", "\n", "\n", "\n", "clusterphase_preferenceOutputsmin_phase2__calc__force_max\n", - "\n", - "min_phase2__calc__force_max\n", + "\n", + "min_phase2__calc__force_max\n", "\n", "\n", "\n", "clusterphase_preferenceOutputsmin_phase2__calc__forces\n", - "\n", - "min_phase2__calc__forces\n", + "\n", + "min_phase2__calc__forces\n", "\n", "\n", "\n", "clusterphase_preferenceOutputsmin_phase2__calc__indices\n", - "\n", - "min_phase2__calc__indices\n", + "\n", + "min_phase2__calc__indices\n", "\n", "\n", "\n", "clusterphase_preferenceOutputsmin_phase2__calc__positions\n", - "\n", - "min_phase2__calc__positions\n", + "\n", + "min_phase2__calc__positions\n", "\n", "\n", "\n", "clusterphase_preferenceOutputsmin_phase2__calc__pressures\n", - "\n", - "min_phase2__calc__pressures\n", + "\n", + "min_phase2__calc__pressures\n", "\n", "\n", "\n", "clusterphase_preferenceOutputsmin_phase2__calc__steps\n", - "\n", - "min_phase2__calc__steps\n", + "\n", + "min_phase2__calc__steps\n", "\n", "\n", "\n", "clusterphase_preferenceOutputsmin_phase2__calc__total_displacements\n", - "\n", - "min_phase2__calc__total_displacements\n", + "\n", + "min_phase2__calc__total_displacements\n", "\n", "\n", "\n", "clusterphase_preferenceOutputsmin_phase2__calc__unwrapped_positions\n", - "\n", - "min_phase2__calc__unwrapped_positions\n", + "\n", + "min_phase2__calc__unwrapped_positions\n", "\n", "\n", "\n", "clusterphase_preferenceOutputsmin_phase2__calc__volume\n", - "\n", - "min_phase2__calc__volume\n", + "\n", + "min_phase2__calc__volume\n", "\n", "\n", "\n", @@ -2745,28 +2794,28 @@ "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__cells\n", - "\n", - "calc__cells\n", + "\n", + "calc__cells\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__cells->clusterphase_preferenceOutputsmin_phase2__calc__cells\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__displacements\n", - "\n", - "calc__displacements\n", + "\n", + "calc__displacements\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__displacements->clusterphase_preferenceOutputsmin_phase2__calc__displacements\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -2790,132 +2839,132 @@ "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__energy_tot\n", - "\n", - "calc__energy_tot\n", + "\n", + "calc__energy_tot\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__energy_tot->clusterphase_preferenceOutputsmin_phase2__calc__energy_tot\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__force_max\n", - "\n", - "calc__force_max\n", + "\n", + "calc__force_max\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__force_max->clusterphase_preferenceOutputsmin_phase2__calc__force_max\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__forces\n", - "\n", - "calc__forces\n", + "\n", + "calc__forces\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__forces->clusterphase_preferenceOutputsmin_phase2__calc__forces\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__indices\n", - "\n", - "calc__indices\n", + "\n", + "calc__indices\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__indices->clusterphase_preferenceOutputsmin_phase2__calc__indices\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__positions\n", - "\n", - "calc__positions\n", + "\n", + "calc__positions\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__positions->clusterphase_preferenceOutputsmin_phase2__calc__positions\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__pressures\n", - "\n", - "calc__pressures\n", + "\n", + "calc__pressures\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__pressures->clusterphase_preferenceOutputsmin_phase2__calc__pressures\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__steps\n", - "\n", - "calc__steps\n", + "\n", + "calc__steps\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__steps->clusterphase_preferenceOutputsmin_phase2__calc__steps\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__total_displacements\n", - "\n", - "calc__total_displacements\n", + "\n", + "calc__total_displacements\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__total_displacements->clusterphase_preferenceOutputsmin_phase2__calc__total_displacements\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__unwrapped_positions\n", - "\n", - "calc__unwrapped_positions\n", + "\n", + "calc__unwrapped_positions\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__unwrapped_positions->clusterphase_preferenceOutputsmin_phase2__calc__unwrapped_positions\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__volume\n", - "\n", - "calc__volume\n", + "\n", + "calc__volume\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__volume->clusterphase_preferenceOutputsmin_phase2__calc__volume\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -2947,10 +2996,10 @@ "\n" ], "text/plain": [ - "" + "" ] }, - "execution_count": 36, + "execution_count": 37, "metadata": {}, "output_type": "execute_result" } @@ -2961,7 +3010,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 38, "id": "b51bef25-86c5-4d57-80c1-ab733e703caf", "metadata": {}, "outputs": [ @@ -2982,7 +3031,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 39, "id": "091e2386-0081-436c-a736-23d019bd9b91", "metadata": {}, "outputs": [ @@ -3023,7 +3072,7 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 40, "id": "4cdffdca-48d3-4486-9045-48102c7e5f31", "metadata": {}, "outputs": [ @@ -3061,7 +3110,7 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": 41, "id": "ed4a3a22-fc3a-44c9-9d4f-c65bc1288889", "metadata": {}, "outputs": [ @@ -3091,7 +3140,7 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 42, "id": "5a985cbf-c308-4369-9223-b8a37edb8ab1", "metadata": {}, "outputs": [ @@ -3119,6 +3168,22 @@ "print(f\"{wf.inputs.element.value}: E({wf.inputs.phase2.value}) - E({wf.inputs.phase1.value}) = {out.compare__de:.2f} eV/atom\")" ] }, + { + "cell_type": "markdown", + "id": "e5e75718-137f-43e6-b484-5ebe4ab22bf8", + "metadata": {}, + "source": [ + "Now that we have nested macros, we can finally discuss the subtle difference between `__call__` and `pull`:\n", + "\n", + "Each `Macro` instance is its own little walled garden, where it's child nodes have no connections apart from those to other children of the same macro (you can forceably change this, since we're all adults here, but it won't happen by default and isn't recommended). Under the hood this is accomplished by the macro IO \"linking\" itself to its childrens' IO, so that updates to macro input values are always immediately propagated to children, and macro output gets synchronized with its childrens' output at the end of every run. Because of this we can think of these children all having the same \"scope\", i.e. siblings among the same parent.\n", + "\n", + "`pull` has a keyword argument to determine whether upstream data dependencies are restricted to be _in scope_, or if the parent node (if any) should also consider all _its_ data dependencies as well, and so on up until we hit the parent-most macro or workflow.\n", + "\n", + "For `pull` this parameter defaults to `False`, so that the pull stops at the parent node. For `__call__` it defaults to `True`, so that the search for data dependencies punches right through parents and all the way up. The danger is that this might be expensive if there's an costly node somewhere in the dependency!\n", + "\n", + "Note that the entire \"pull\" paradigm does currently play nicely with remote execution. If some of your nodes have an executor specified, you will need to `.run` your graph (or `__call__` a `Workflow` if that's your parent-most object)." + ] + }, { "cell_type": "markdown", "id": "f447531e-3e8c-4c7e-a579-5f9c56b75a5b", @@ -3136,10 +3201,11 @@ "source": [ "## Parallelization\n", "\n", - "You can currently run _some_ nodes (namely, `Function` nodes that don't take `self` as an argument) in a background process by setting an `executor` of the right type.\n", - "Cf. the `Workflow` class tests in the source code for an example.\n", + "You can currently run nodes in a single-core background process by setting that node's `executor` to `True`. The plan is to eventually lean on `pympipool` for more powerful executors that allow for multiple cores, interaction with HPC clusters, etc. We may also leverage the `Submitter` in `pyiron_contrib.tinybase` so that multiple nodes can lean on the same resources.\n", "\n", - "Right now our treatment of DAGs is quite rudimentary, and the data flow is (unless cyclic) converted into a _linear_ execution pattern. \n", + "Unfortunately, _nested_ executors are not yet working. So if you set a macro to use an executor, none of its (grand...)children may specify an executor.\n", + "\n", + "Note also that right now our treatment of DAGs is quite rudimentary, and the data flow is (unless cyclic) converted into a _linear_ execution pattern. \n", "This is practical and robust, but highly inefficient when combined with nodes that can run in parallel, i.e. with \"executors\".\n", "Going forward, we will exploit the same infrastructure of data flow DAGs and run signals to build up more sophisticated execution patterns which support parallelization." ] @@ -3151,9 +3217,9 @@ "source": [ "## Serialization and node libraries\n", "\n", - "Serialization doesn't exist yet.\n", + "Serialization for storage doesn't exist yet.\n", "\n", - "What you _can_ do is `register` new lists of nodes (including macros) with the workflow, so feel free to build up your own `.py` files containing nodes you like to use for easy re-use. Registration is now discussed in the main body of the notebook, but the API may change significantly going forward.\n", + "What you _can_ do is `register` new modules that have a list of nodes (including macros) with the workflow, so feel free to build up your own `.py` files containing nodes you like to use for easy re-use. Registration is now discussed in the main body of the notebook, but the API may change significantly going forward.\n", "\n", "Serialization of workflows is still forthcoming, while for node registration flexibility and documentation is forthcoming but the basics are here already." ] @@ -3181,7 +3247,7 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 43, "id": "0b373764-b389-4c24-8086-f3d33a4f7fd7", "metadata": {}, "outputs": [ @@ -3195,7 +3261,7 @@ " 17.230249999999995]" ] }, - "execution_count": 42, + "execution_count": 43, "metadata": {}, "output_type": "execute_result" } @@ -3232,7 +3298,7 @@ }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 44, "id": "0dd04b4c-e3e7-4072-ad34-58f2c1e4f596", "metadata": {}, "outputs": [ @@ -3291,7 +3357,7 @@ }, { "cell_type": "code", - "execution_count": 44, + "execution_count": 45, "id": "2dfb967b-41ac-4463-b606-3e315e617f2a", "metadata": {}, "outputs": [ @@ -3315,7 +3381,7 @@ }, { "cell_type": "code", - "execution_count": 45, + "execution_count": 46, "id": "2e87f858-b327-4f6b-9237-c8a557f29aeb", "metadata": {}, "outputs": [ @@ -3323,8 +3389,18 @@ "name": "stdout", "output_type": "stream", "text": [ - "0.012 <= 0.2\n", - "Finally 0.012\n" + "0.639 > 0.2\n", + "0.481 > 0.2\n", + "0.582 > 0.2\n", + "0.213 > 0.2\n", + "0.829 > 0.2\n", + "0.826 > 0.2\n", + "0.401 > 0.2\n", + "0.929 > 0.2\n", + "0.251 > 0.2\n", + "0.525 > 0.2\n", + "0.087 <= 0.2\n", + "Finally 0.087\n" ] } ], diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 3b1fce86..3dfab32e 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -41,16 +41,17 @@ class Composite(Node, ABC): instances, any created nodes get their `parent` attribute automatically set to the composite instance being used. - Specifies the required `on_run()` to call `run()` on a subset of owned - `starting_nodes`nodes to kick-start computation on the owned sub-graph. + Specifies the required `on_run()` and `run_args` to call `run()` on a subset of + owned `starting_nodes`, thus kick-starting computation on the owned sub-graph. Both the specification of these starting nodes and specifying execution signals to propagate execution through the graph is left to the user/child classes. In the case of non-cyclic workflows (i.e. DAGs in terms of data flow), both - starting nodes and execution flow can be specified by invoking `` + starting nodes and execution flow can be specified by invoking execution flow can + be determined automatically. - The `run()` method (and `update()`, and calling the workflow) return a new - dot-accessible dictionary of keys and values created from the composite output IO - panel. + Also specifies `process_run_result` such that the `run` method (and its aliases) + return a new dot-accessible dictionary of keys and values created from the + composite output IO panel. Does not specify `input` and `output` as demanded by the parent class; this requirement is still passed on to children. @@ -81,10 +82,6 @@ class Composite(Node, ABC): add(node: Node): Add the node instance to this subgraph. remove(node: Node): Break all connections the node has, remove it from this subgraph, and set its parent to `None`. - - TODO: - Wrap node registration at the class level so we don't need to do - `X.create.register` but can just do `X.register` """ wrap_as = Wrappers() diff --git a/pyiron_workflow/function.py b/pyiron_workflow/function.py index 058f79e7..3face365 100644 --- a/pyiron_workflow/function.py +++ b/pyiron_workflow/function.py @@ -47,7 +47,7 @@ class Function(Node): Actual function node instances can either be instances of the base node class, in which case the callable node function *must* be provided OR they can be instances - of children of this class. + of children of this class which provide the node function as a class-level method. Those children may define some or all of the node behaviour at the class level, and modify their signature accordingly so this is not available for alteration by the user, e.g. the node function and output labels may be hard-wired. @@ -65,10 +65,8 @@ class Function(Node): After a node is instantiated, its input can be updated as `*args` and/or `**kwargs` on call. - `run()` returns the output of the executed function, or a futures object if the - node is set to use an executor. - Calling the node or executing an `update()` returns the same thing as running, if - the node is run, or, in the case of `update()`, `None` if it is not `ready` to run. + `run()` and its aliases return the output of the executed function, or a futures + object if the node is set to use an executor. Args: node_function (callable): The function determining the behaviour of the node. @@ -88,23 +86,6 @@ class Function(Node): **kwargs: Any additional keyword arguments whose keyword matches the label of an input channel will have their value assigned to that channel. - Attributes: - inputs (Inputs): A collection of input data channels. - outputs (Outputs): A collection of output data channels. - signals (Signals): A holder for input and output collections of signal channels. - ready (bool): All input reports ready, node is not running or failed. - running (bool): Currently running. - failed (bool): An exception was thrown when executing the node function. - connected (bool): Any IO channel has at least one connection. - fully_connected (bool): Every IO channel has at least one connection. - - Methods: - update: If your input is ready, will run the engine. - run: Parse and process the input, execute the engine, process the results and - update the output. - disconnect: Disconnect all data and signal IO connections. - set_input_values: Allows input channels' values to be updated without any running. - Examples: At the most basic level, to use nodes all we need to do is provide the `Function` class with a function and labels for its output, like so: @@ -127,10 +108,15 @@ class Function(Node): run: >>> plus_minus_1.inputs.x = 2 >>> plus_minus_1.run() - TypeError: unsupported operand type(s) for -: 'type' and 'int' + ValueError: mwe received a run command but is not ready. The node should be + neither running nor failed, and all input values should conform to type hints: + running: False + failed: False + x ready: True + y ready: False - This is because the second input (`y`) still has no input value, so we can't do - the sum between `NotData` and `2`. + This is because the second input (`y`) still has no input value -- indicated in + the error message -- so we can't do the sum between `NotData` and `2`. Once we update `y`, all the input is ready we will be allowed to proceed to a `run()` call, which succeeds and updates the output. @@ -152,9 +138,8 @@ class Function(Node): Input data can be provided to both initialization and on call as ordered args or keyword kwargs. - When running, updating, or calling the node, the output of the wrapped function - (if it winds up getting run in the conditional cases of updating and calling) is - returned: + When running the node (or any alias to run like pull, execute, or just calling + the node), the output of the wrapped function is returned: >>> plus_minus_1(2, y=3) (3, 2) @@ -172,7 +157,7 @@ class Function(Node): Note that getting "good" (i.e. dot-accessible) output labels can be achieved by using good variable names and returning those variables instead of using `output_labels`. - If we force the node to `run()` (or call it) with bad types, it will raise an + If we force the node to run with bad types, it will raise an error: >>> from typing import Union >>> @@ -259,24 +244,55 @@ class Function(Node): Finally, let's put it all together by using both of these nodes at once. Instead of setting input to a particular data value, we'll set it to be another node's output channel, thus forming a connection. - Then we need to define the corresponding execution flow, which can be done - by directly connecting `.signals.input.run` and `.signals.output.ran` channels - just like we connect data channels, but can also be accomplished with some - syntactic sugar using the `>` operator. - When we update the upstream node, we'll see the result passed downstream: - >>> adder = Adder() + At the end of the day, the graph will also need to know about the execution + flow, but in most cases (directed acyclic graphs -- DAGs), this can be worked + out automatically by the topology of data connections. + Let's put together a couple of nodes and then run in a "pull" paradigm to get + the final node to run everything "upstream" then run itself: + >>> @function_node() + ... def adder_node(x: int = 0, y: int = 0) -> int: + ... sum = x + y + ... return sum + >>> + >>> adder = adder_node(x=1) + >>> alpha = AlphabetModThree(i=adder.outputs.sum) + >>> print(alpha()) + "b" + >>> adder.inputs.y = 1 + >>> print(alpha()) + "c" + >>> adder.inputs.x = 0 + >>> adder.inputs.y = 0 + >>> print(alpha()) + "a" + + Alternatively, execution flows can be specified manualy by connecting + `.signals.input.run` and `.signals.output.ran` channels, either by their + `.connect` method or by assignment (both cases just like data chanels), or by + some syntactic sugar using the `>` operator. + Then we can use a "push" paradigm with the `run` command to force execution + forwards through the graph to get an end result. + This is a bit more verbose, but a necessary tool for more complex situations + (like cyclic graphs). + Here's our simple example from above using this other paradigm: + >>> @function_node() + ... def adder_node(x: int = 0, y: int = 0) -> int: + ... sum = x + y + ... return sum + >>> + >>> adder = adder_node() >>> alpha = AlphabetModThree(i=adder.outputs.sum) >>> adder > alpha >>> - >>> adder(x=1) + >>> adder.run(x=1) >>> print(alpha.outputs.letter) "b" - >>> adder(y=1) + >>> adder.run(y=1) >>> print(alpha.outputs.letter) "c" >>> adder.inputs.x = 0 >>> adder.inputs.y = 0 - >>> adder() + >>> adder.run() >>> print(alpha.outputs.letter) "a" @@ -285,16 +301,7 @@ class Function(Node): Comments: - If you use the function argument `self` in the first position, the - whole node object is inserted there: - - >>> def with_self(self, x): - >>> ... - >>> return x - - For this function, you don't have the freedom to choose `self`, because - pyiron automatically sets the node object there (which is also the - reason why you do not see `self` in the list of inputs). + Using the `self` argument for function nodes is not currently supported. """ def __init__( @@ -532,6 +539,14 @@ def set_input_values(self, *args, **kwargs) -> None: kwargs = self._convert_input_args_and_kwargs_to_input_kwargs(*args, **kwargs) return super().set_input_values(**kwargs) + def execute(self, *args, **kwargs): + kwargs = self._convert_input_args_and_kwargs_to_input_kwargs(*args, **kwargs) + return super().execute(**kwargs) + + def pull(self, *args, run_parent_trees_too=False, **kwargs): + kwargs = self._convert_input_args_and_kwargs_to_input_kwargs(*args, **kwargs) + return super().pull(run_parent_trees_too=run_parent_trees_too, **kwargs) + def __call__(self, *args, **kwargs) -> None: kwargs = self._convert_input_args_and_kwargs_to_input_kwargs(*args, **kwargs) return super().__call__(**kwargs) @@ -561,6 +576,9 @@ class SingleValue(Function, HasChannel): Note that this means any attributes/method available on the output value become available directly at the node level (at least those which don't conflict with the existing node namespace). + + This also allows the entire node to be used as a reference to its output channel + when making data connections, e.g. `some_node.input.some_channel = my_svn_instance`. """ def __init__( diff --git a/pyiron_workflow/macro.py b/pyiron_workflow/macro.py index e81d4975..b68eaaf2 100644 --- a/pyiron_workflow/macro.py +++ b/pyiron_workflow/macro.py @@ -35,7 +35,7 @@ class Macro(Composite): It is intended that subclasses override the initialization signature and provide the graph creation directly from their own method. - As with workflows, all DAG macros will determine their execution flow automatically, + As with workflows, all DAG macros can determine their execution flow automatically, if you have cycles in your data flow, or otherwise want more control over the execution, all you need to do is specify the `node.signals.input.run` connections and `starting_nodes` list yourself. @@ -155,7 +155,7 @@ class Macro(Composite): >>> adds_six_macro.two.replace_with(add_two()) >>> # And by assignment of a compatible class to an occupied node label >>> adds_six_macro.three = add_two - >>> adds_six_macro(inp=1) + >>> adds_six_macro(one__x=1) {'three__result': 7} """ diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index d2f7a78b..14f33aae 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -16,7 +16,10 @@ from pyiron_workflow.files import DirectoryObject from pyiron_workflow.has_to_dict import HasToDict from pyiron_workflow.io import Signals, InputSignal, OutputSignal -from pyiron_workflow.topology import set_run_connections_according_to_linear_dag +from pyiron_workflow.topology import ( + get_nodes_in_data_tree, + set_run_connections_according_to_linear_dag, +) from pyiron_workflow.util import SeabornColors if TYPE_CHECKING: @@ -69,9 +72,14 @@ class Node(HasToDict, ABC): Together these channels represent edges on the dual data and execution computational graphs. - Nodes can be run to force their computation, or more gently updated, which will - trigger a run only if all of the input is ready (i.e. channel values conform to - any type hints provided). + Nodes can be run in a variety of ways.. + Non-exhaustively, they can be run in a "push" paradigm where they do their + calculation and then trigger downstream calculations; in a "pull" mode where they + first make sure all their upstream dependencies then run themselves (but not + anything downstream); or they may be forced to run their calculation with exactly + the input they have right now. + These and more options are available, and for more information look at the `run` + method. Nodes may have a `parent` node that owns them as part of a sub-graph. @@ -92,15 +100,15 @@ class Node(HasToDict, ABC): channels. The `run()` method returns a representation of the node output (possible a futures - object, if the node is running on an executor), and consequently `update()` also - returns this output if the node is `ready`. Both `run()` and `update()` will raise - errors if the node is already running or has a failed status. + object, if the node is running on an executor), and consequently the `pull`, + `execute`, and `__call__` shortcuts to `run` also return the same thing. - Calling an already instantiated node allows its input channels to be updated using - keyword arguments corresponding to the channel labels, performing a batch-update of - all supplied input and then calling `run()`. - As such, calling the node _also_ returns a representation of the output (or `None` - if the node is not set to run on updates, or is otherwise unready to run). + Invoking the `run` method (or one of its aliases) of an already instantiated node + allows its input channels to be updated using keyword arguments corresponding to + the channel labels, performing a batch-update of all supplied input and then + proceeding. + As such, _if_ the run invocation updates the input values some other way, these + supplied values will get overwritten. Nodes have a status, which is currently represented by the `running` and `failed` boolean flag attributes. @@ -116,15 +124,16 @@ class Node(HasToDict, ABC): with the resulting future object. WARNING: Executors are currently only working when the node executable function does not use `self`. + NOTE: Executors are only allowed in a "push" paradigm, and you will get an + exception if you try to `pull` and one of the upstream nodes uses an executor. This is an abstract class. - Children *must* define how `inputs` and `outputs` are constructed, and what will - happen `on_run`. - They may also override the `run_args` property to specify input passed to the - defined `on_run` method, and may add additional signal channels to the signals IO. + Children *must* define how `inputs` and `outputs` are constructed, what will + happen `on_run`, the `run_args` that will get passed to `on_run`, and how to + `process_run_result` once `on_run` finishes. + They may optionally add additional signal channels to the signals IO. - # TODO: Everything with (de)serialization and executors for running on something - # other than the main python process. + # TODO: Everything with (de)serialization for storage Attributes: connected (bool): Whether _any_ of the IO (including signals) are connected. @@ -157,20 +166,24 @@ class Node(HasToDict, ABC): initialized. Methods: - __call__: Update input values (optional) then run the node (without firing off - .the `ran` signal, so nothing happens farther downstream). + __call__: An alias for `pull` that aggressively runs upstream nodes even + _outside_ the local scope (i.e. runs parents' dependencies as well). disconnect: Remove all connections, including signals. draw: Use graphviz to visualize the node, its IO and, if composite in nature, its internal structure. - execute: Run the node, but right here, right now, and with the input it - currently has. + execute: An alias for `run`, but with flags to run right here, right now, and + with the input it currently has. on_run: **Abstract.** Do the thing. What thing must be specified by child classes. - pull: Run everything upstream, then run this node (but don't fire off the `ran` - signal, so nothing happens farther downstream). - run: Run the node function from `on_run`. Handles status, whether to run on an - executor, firing the `ran` signal, and callbacks (if an executor is used). - set_input_values: Allows input channels' values to be updated without any running. + pull: An alias for `run` that runs everything upstream, then runs this node + (but doesn't fire off the `ran` signal, so nothing happens farther + downstream). "Upstream" may optionally break out of the local scope to run + parent nodes' dependencies as well (all the way until the parent-most + object is encountered). + run: Run the node function from `on_run`. Handles status automatically. Various + execution options are available as boolean flags. + set_input_values: Allows input channels' values to be updated without any + running. """ def __init__( @@ -245,35 +258,66 @@ def process_run_result(self, run_output): def run( self, - first_fetch_input: bool = True, - then_emit_output_signals: bool = True, - force_local_execution: bool = False, + run_data_tree: bool = False, + run_parent_trees_too: bool = False, + fetch_input: bool = True, check_readiness: bool = True, + force_local_execution: bool = False, + emit_ran_signal: bool = True, + **kwargs, ): """ - Update the input (with whatever is currently available -- does _not_ trigger - any other nodes to run) and use it to perform the node's operation. After, - emit all output signals. + The master method for running in a variety of ways. + By default, whatever data is currently available in upstream nodes will be + fetched, if the input all conforms to type hints then this node will be run + (perhaps using an executor), and finally the `ran` signal will be emitted to + trigger downstream runs. If executor information is specified, execution happens on that process, a callback is registered, and futures object is returned. + Input values can be updated at call time with kwargs, but this happens _first_ + so any input updates that happen as a result of the computation graph will + override these by default. If you really want to execute the node with a + particular set of input, set it all manually and use `execute` (or `run` with + carefully chosen flags). + Args: - first_fetch_input (bool): Whether to first update inputs with the + run_data_tree (bool): Whether to first run all upstream nodes in the data + graph. (Default is False.) + run_parent_trees_too (bool): Whether to recursively run the data tree in + parent nodes (if any). (Default is False.) + fetch_input (bool): Whether to first update inputs with the highest-priority connections holding data. (Default is True.) - then_emit_output_signals (bool): Whether to fire off all output signals - (e.g. `ran`) afterwards. (Default is True.) - force_local_execution (bool): Whether to ignore any executor settings and - force the computation to run locally. (Default is False.) check_readiness (bool): Whether to raise an exception if the node is not `ready` to run after fetching new input. (Default is True.) + force_local_execution (bool): Whether to ignore any executor settings and + force the computation to run locally. (Default is False.) + emit_ran_signal (bool): Whether to fire off all the output `ran` signal + afterwards. (Default is True.) + **kwargs: Keyword arguments matching input channel labels; used to update + the input channel values before running anything. Returns: (Any | Future): The result of running the node, or a futures object (if running on an executor). + + Note: + Running data trees is a pull-based paradigm and only compatible with graphs + whose data forms a directed acyclic graph (DAG). + + Note: + Kwargs updating input channel values happens _first_ and will get + overwritten by any subsequent graph-based data manipulation. """ - if first_fetch_input: + self.set_input_values(**kwargs) + + if run_data_tree: + self.run_data_tree(run_parent_trees_too=run_parent_trees_too) + + if fetch_input: self.inputs.fetch() + if check_readiness and not self.ready: input_readiness = "\n".join( [f"{k} ready: {v.ready}" for k, v in self.inputs.items()] @@ -285,13 +329,77 @@ def run( f"running: {self.running}\n" f"failed: {self.failed}\n" + input_readiness ) + return self._run( finished_callback=self._finish_run_and_emit_ran - if then_emit_output_signals + if emit_ran_signal else self._finish_run, force_local_execution=force_local_execution, ) + def run_data_tree(self, run_parent_trees_too=False) -> None: + """ + Use topological analysis to build a tree of all upstream dependencies and run + them. + + Args: + run_parent_trees_too (bool): First, call the same method on this node's + parent (if one exists), and recursively up the parentage tree. (Default + is False, only run nodes in this scope, i.e. sharing the same parent.) + """ + if run_parent_trees_too and self.parent is not None: + self.parent.run_data_tree(run_parent_trees_too=True) + self.parent.inputs.fetch() + + label_map = {} + nodes = {} + + data_tree_nodes = get_nodes_in_data_tree(self) + for node in data_tree_nodes: + if node.executor: + raise ValueError( + f"Running the data tree is pull-paradigm action, and is " + f"incompatible with using executors. An executor request was found " + f"on {node.label}" + ) + + for node in data_tree_nodes: + modified_label = node.label + str(id(node)) + label_map[modified_label] = node.label + node.label = modified_label # Ensure each node has a unique label + # This is necessary when the nodes do not have a workflow and may thus have + # arbitrary labels. + # This is pretty ugly; it would be nice to not depend so heavily on labels. + # Maybe we could switch a bunch of stuff to rely on the unique ID? + nodes[modified_label] = node + + try: + disconnected_pairs, starter = set_run_connections_according_to_linear_dag( + nodes + ) + except Exception as e: + # If the dag setup fails it will repair any connections it breaks before + # raising the error, but we still need to repair our label changes + for modified_label, node in nodes.items(): + node.label = label_map[modified_label] + raise e + + self.signals.disconnect_run() + # Don't let anything upstream trigger this node + + try: + # If you're the only one in the data tree, there's nothing upstream to run + # Otherwise... + if starter is not self: + starter.run() # Now push from the top + finally: + # No matter what, restore the original connections and labels afterwards + for modified_label, node in nodes.items(): + node.label = label_map[modified_label] + node.signals.disconnect_run() + for c1, c2 in disconnected_pairs: + c1.connect(c2) + @manage_status def _run( self, @@ -345,78 +453,55 @@ def _finish_run_and_emit_ran(self, run_output: tuple | Future) -> Any | tuple: """ ) - def execute(self): + def execute(self, **kwargs): """ - Run the node with whatever input it currently has, run it on this python - process, and don't emit the `ran` signal afterwards. + A shortcut for `run` with particular flags. + + Run the node with whatever input it currently has (or is given as kwargs here), + run it on this python process, and don't emit the `ran` signal afterwards. Intended to be useful for debugging by just forcing the node to do its thing right here, right now, and as-is. """ return self.run( - first_fetch_input=False, - then_emit_output_signals=False, - force_local_execution=True, + run_data_tree=False, + run_parent_trees_too=False, + fetch_input=False, check_readiness=False, + force_local_execution=True, + emit_ran_signal=False, + **kwargs, ) - def pull(self): + def pull(self, run_parent_trees_too=False, **kwargs): """ - Use topological analysis to build a tree of all upstream dependencies; run them - first, then run this node to get an up-to-date result. Does not trigger any - downstream executions. - """ - label_map = {} - nodes = {} - for node in self.get_nodes_in_data_tree(): - modified_label = node.label + str(id(node)) - label_map[modified_label] = node.label - node.label = modified_label # Ensure each node has a unique label - # This is necessary when the nodes do not have a workflow and may thus have - # arbitrary labels. - # This is pretty ugly; it would be nice to not depend so heavily on labels. - # Maybe we could switch a bunch of stuff to rely on the unique ID? - nodes[modified_label] = node - disconnected_pairs, starter = set_run_connections_according_to_linear_dag(nodes) - try: - self.signals.disconnect_run() # Don't let anything upstream trigger this - starter.run() # Now push from the top - return self.run() # Finally, run here and return the result - # Emitting won't matter since we already disconnected this one - finally: - # No matter what, restore the original connections and labels afterwards - for modified_label, node in nodes.items(): - node.label = label_map[modified_label] - node.signals.disconnect_run() - for c1, c2 in disconnected_pairs: - c1.connect(c2) + A shortcut for `run` with particular flags. - def get_nodes_in_data_tree(self) -> set[Node]: - """ - Get a set of all nodes from this one and upstream through data connections. + Runs nodes upstream in the data graph, then runs this node without triggering + any downstream runs. By default only runs sibling nodes, but can optionally + require the parent node to pull in its own upstream runs (this is recursive + up to the parent-most object). + + Args: + run_parent_trees_too (bool): Whether to (recursively) require the parent to + first pull. """ - nodes = set([self]) - for channel in self.inputs: - for connection in channel.connections: - nodes = nodes.union(connection.node.get_nodes_in_data_tree()) - return nodes + return self.run( + run_data_tree=True, + run_parent_trees_too=run_parent_trees_too, + fetch_input=True, + check_readiness=True, + force_local_execution=False, + emit_ran_signal=False, + **kwargs, + ) def __call__(self, **kwargs) -> None: """ - Update the input, then run without firing the `ran` signal. - - Note that since input fetching happens _after_ the input values are updated, - if there is a connected data value it will get used instead of what is specified - here. If you really want to set a particular state and then run this can be - accomplished with `.inputs.fetch()` then `.set_input_values(...)` then - `.execute()` (or `.run(...)` with the flags you want). - - Args: - **kwargs: Keyword arguments matching input channel labels; used to update - the input before running. + A shortcut for `pull` that automatically runs the entire set of upstream data + dependencies all the way to the parent-most graph object. """ - self.set_input_values(**kwargs) - return self.run() + return self.pull(run_parent_trees_too=True, **kwargs) def set_input_values(self, **kwargs) -> None: """ diff --git a/pyiron_workflow/topology.py b/pyiron_workflow/topology.py index 06d53631..baaf8c09 100644 --- a/pyiron_workflow/topology.py +++ b/pyiron_workflow/topology.py @@ -15,6 +15,10 @@ from pyiron_workflow.node import Node +class CircularDataFlowError(ValueError): + pass + + def nodes_to_data_digraph(nodes: dict[str, Node]) -> dict[str, set[str]]: """ Maps a set of nodes to a digraph of their data dependency in the format of label @@ -29,7 +33,7 @@ def nodes_to_data_digraph(nodes: dict[str, Node]) -> dict[str, set[str]]: data. Raises: - ValueError: When a node appears in its own input. + CircularDataFlowError: When a node appears in its own input. ValueError: If the nodes do not all have the same parent. ValueError: If one of the nodes has an upstream data connection whose node has a different parent. @@ -72,7 +76,7 @@ def nodes_to_data_digraph(nodes: dict[str, Node]) -> dict[str, set[str]]: # the toposort library has a # [known issue](https://gitlab.com/ericvsmith/toposort/-/issues/3) # That self-dependency isn't caught, so we catch it manually here. - raise ValueError( + raise CircularDataFlowError( f"Detected a cycle in the data flow topology, unable to automate " f"the execution of non-DAGs: {node.label} appears in its own input." ) @@ -95,7 +99,7 @@ def nodes_to_execution_order(nodes: dict[str, Node]) -> list[str]: (list[str]): The labels in safe execution order. Raises: - CircularDependencyError: If the data dependency is not a Directed Acyclic Graph + ValueError: If the data dependency is not a Directed Acyclic Graph """ try: # Topological sorting ensures that all input dependencies have been @@ -104,10 +108,10 @@ def nodes_to_execution_order(nodes: dict[str, Node]) -> list[str]: # generations that are mutually independent (inefficient but easier for now) execution_order = toposort_flatten(nodes_to_data_digraph(nodes)) except CircularDependencyError as e: - raise ValueError( + raise CircularDataFlowError( f"Detected a cycle in the data flow topology, unable to automate the " f"execution of non-DAGs: cycles found among {e.data}" - ) + ) from e return execution_order @@ -154,3 +158,21 @@ def set_run_connections_according_to_linear_dag( for c1, c2 in disconnected_pairs: c1.connect(c2) raise e + + +def get_nodes_in_data_tree(node: Node) -> set[Node]: + """ + Get a set of all nodes from this one and upstream through data connections. + """ + try: + nodes = set([node]) + for channel in node.inputs: + for connection in channel.connections: + nodes = nodes.union(get_nodes_in_data_tree(connection.node)) + return nodes + except RecursionError: + raise CircularDataFlowError( + f"Detected a cycle in the data flow topology, unable to automate the " + f"execution of non-DAGs: finding the upstream nodes for {node.label} hit a " + f"recursion error." + ) diff --git a/pyiron_workflow/workflow.py b/pyiron_workflow/workflow.py index 36eee604..2b7611a6 100644 --- a/pyiron_workflow/workflow.py +++ b/pyiron_workflow/workflow.py @@ -29,8 +29,8 @@ class Workflow(Composite): They are then accessible either under the `nodes` dot-dictionary, or just directly by dot-access on the workflow object itself. - Using the `input` and `output` attributes, the workflow gives access to all the - IO channels among its nodes which are currently unconnected. + Using the `input` and `output` attributes, the workflow gives by-reference access + to all the IO channels among its nodes which are currently unconnected. The `Workflow` class acts as a single-point-of-import for us; Directly from the class we can use the `create` method to instantiate workflow @@ -38,6 +38,14 @@ class Workflow(Composite): When called from a workflow _instance_, any created nodes get their parent set to the workflow instance being used. + Workflows are "living" -- i.e. their IO is always by reference to their owned nodes + and you are meant to add and remove nodes as children -- and "parent-most" -- i.e. + they sit at the top of any data dependency tree and may never have a parent of + their own. + They are flexible and great for development, but once you have a setup you like, + you should consider reformulating it as a `Macro`, which operates somewhat more + efficiently. + Examples: We allow adding nodes to workflows in five equivalent ways: >>> from pyiron_workflow.workflow import Workflow @@ -116,8 +124,10 @@ class Workflow(Composite): 12 Workflows also give access to packages of pre-built nodes under different - namespaces, e.g. + namespaces. These need to be registered first. >>> wf = Workflow("with_prebuilt") + >>> wf.register("atomistics", "pyiron_workflow.node_library.atomistics") + >>> wf.register("plotting", "pyiron_workflow.node_library.plotting") >>> >>> wf.structure = wf.create.atomistics.Bulk( ... cubic=True, @@ -127,7 +137,7 @@ class Workflow(Composite): >>> wf.calc = wf.create.atomistics.CalcMd( ... job=wf.engine, ... ) - >>> wf.plot = wf.create.standard.Scatter( + >>> wf.plot = wf.create.plotting.Scatter( ... x=wf.calc.outputs.steps, ... y=wf.calc.outputs.temperature ... ) @@ -205,14 +215,28 @@ def outputs(self) -> Outputs: def run( self, - first_fetch_input: bool = True, - then_emit_output_signals: bool = True, - force_local_execution: bool = False, check_readiness: bool = True, + force_local_execution: bool = False, + **kwargs, ): + # Note: Workflows may have neither parents nor siblings, so we don't need to + # worry about running their data trees first, fetching their input, nor firing + # their `ran` signal, hence the change in signature from Node.run if self.automate_execution: self.set_run_signals_to_dag_execution() - return super().run() + return super().run( + run_data_tree=False, + run_parent_trees_too=False, + fetch_input=False, + check_readiness=check_readiness, + force_local_execution=force_local_execution, + emit_ran_signal=False, + **kwargs, + ) + + def pull(self, run_parent_trees_too=False, **kwargs): + """Workflows are a parent-most object, so this simply runs without pulling.""" + return self.run(**kwargs) def to_node(self): """ diff --git a/tests/integration/test_pull.py b/tests/integration/test_pull.py deleted file mode 100644 index 75e85aca..00000000 --- a/tests/integration/test_pull.py +++ /dev/null @@ -1,84 +0,0 @@ -import unittest - -from pyiron_workflow.workflow import Workflow - - -class TestPullingOutput(unittest.TestCase): - def test_without_workflow(self): - from pyiron_workflow import Workflow - - @Workflow.wrap_as.single_value_node("sum") - def x_plus_y(x: int = 0, y: int = 0) -> int: - return x + y - - node = x_plus_y( - x=x_plus_y(0, 1), - y=x_plus_y(2, 3) - ) - self.assertEqual(6, node.pull()) - - for n in [ - node, - node.inputs.x.connections[0].node, - node.inputs.y.connections[0].node, - ]: - self.assertFalse( - n.signals.connected, - msg="Connections should be unwound after the pull is done" - ) - self.assertEqual( - "x_plus_y", - n.label, - msg="Original labels should be restored after the pull is done" - ) - - def test_pulling_from_inside_a_macro(self): - @Workflow.wrap_as.single_value_node("sum") - def x_plus_y(x: int = 0, y: int = 0) -> int: - # print("EXECUTING") - return x + y - - @Workflow.wrap_as.macro_node() - def b2_leaves_a1_alone(macro): - macro.a1 = x_plus_y(0, 0) - macro.a2 = x_plus_y(0, 1) - macro.b1 = x_plus_y(macro.a1, macro.a2) - macro.b2 = x_plus_y(macro.a2, 10) - - wf = Workflow("demo") - wf.upstream = x_plus_y() - wf.macro = b2_leaves_a1_alone(a2__x=wf.upstream) - - # Pulling b1 -- executes a1, a2, b2 - self.assertEqual(1, wf.macro.b1.pull()) - # >>> EXECUTING - # >>> EXECUTING - # >>> EXECUTING - # >>> 1 - - # Pulling b2 -- executes a2, a1 - self.assertEqual(11, wf.macro.b2.pull()) - # >>> EXECUTING - # >>> EXECUTING - # >>> 11 - - # Updated inputs get reflected in the pull - wf.macro.set_input_values(a1__x=100, a2__x=-100) - self.assertEqual(-89, wf.macro.b2.pull()) - # >>> EXECUTING - # >>> EXECUTING - # >>> -89 - - # Connections are restored after a pull - # Crazy negative value of a2 gets written over by pulling in the upstream - # connection value - # Running wf -- executes upstream, macro (is silent), a1, a2, b1, b2 - out = wf() - self.assertEqual(101, out.macro__b1__sum) - self.assertEqual(11, out.macro__b2__sum) - # >>> EXECUTING - # >>> EXECUTING - # >>> EXECUTING - # >>> EXECUTING - # >>> EXECUTING - # >>> {'macro__b1__sum': 101, 'macro__b2__sum': 11} \ No newline at end of file diff --git a/tests/unit/test_function.py b/tests/unit/test_function.py index 28b8fbb8..1a33fcc1 100644 --- a/tests/unit/test_function.py +++ b/tests/unit/test_function.py @@ -696,6 +696,19 @@ def test_disconnection(self): "on) of all broken connections among input, output, and signals." ) + def test_pulling_without_any_parents(self): + node = SingleValue( + plus_one, + x=SingleValue( + plus_one, + x=SingleValue( + plus_one, + x=2 + ) + ) + ) + self.assertEqual(2 + 1 + 1 + 1, node.pull()) + if __name__ == '__main__': unittest.main() diff --git a/tests/unit/test_macro.py b/tests/unit/test_macro.py index 73bae0f5..0ee9d78b 100644 --- a/tests/unit/test_macro.py +++ b/tests/unit/test_macro.py @@ -7,6 +7,7 @@ from pyiron_workflow.channels import NotData from pyiron_workflow.function import SingleValue from pyiron_workflow.macro import Macro +from pyiron_workflow.topology import CircularDataFlowError def add_one(x): @@ -375,11 +376,9 @@ def test_macro_connections_after_replace(self): # For macro IO channels that weren't connected, we don't really care # If it fails to replace, it had better revert to its original state - macro = Macro(add_three_macro) + macro = Macro(add_three_macro, one__x=0) downstream = SingleValue(add_one, x=macro.outputs.three__result) - macro > downstream - macro(one__x=0) - # Or once pull exists: macro.one__x = 0; downstream.pull() + downstream.pull() self.assertEqual( 0 + (1 + 1 + 1) + 1, downstream.outputs.result.value, @@ -392,7 +391,7 @@ def add_two(x): compatible_replacement = SingleValue(add_two) macro.replace(macro.three, compatible_replacement) - macro(one__x=0) + downstream.pull() self.assertEqual( len(downstream.inputs.x.connections), 1, @@ -457,7 +456,10 @@ def different_signature(x): msg="Failed replacements should get reverted, leaving the replacement in " "its original state" ) - macro(one__x=1) # Fresh input to make sure updates are actually going through + macro > downstream + # If we want to push, we need to define a connection formally + macro.run(one__x=1) + # Fresh input to make sure updates are actually going through self.assertEqual( 1 + (1 + 1 + 2) + 1, downstream.outputs.result.value, @@ -484,7 +486,8 @@ def different_signature(x): def test_with_executor(self): macro = Macro(add_three_macro) downstream = SingleValue(add_one, x=macro.outputs.three__result) - macro > downstream # Later we can just pull() instead + macro > downstream # Manually specify since we'll run the macro but look + # at the downstream output, and none of this is happening in a workflow original_one = macro.one macro.executor = True @@ -495,7 +498,7 @@ def test_with_executor(self): msg="Sanity check that test is in right starting condition" ) - result = macro(one__x=0) + result = macro.run(one__x=0) self.assertIsInstance( result, Future, @@ -552,6 +555,135 @@ def test_with_executor(self): "downstream execution" ) + def test_pulling_from_inside_a_macro(self): + upstream = SingleValue(add_one, x=2) + macro = Macro(add_three_macro, one__x=upstream) + macro.inputs.one__x = 0 # Set value + # Now macro.one.inputs.x has both value and a connection + + print("MACRO ONE INPUT X", macro.one.inputs.x.value, macro.one.inputs.x.connections) + + self.assertEqual( + 0 + 1 + 1, + macro.two.pull(run_parent_trees_too=False), + msg="Without running parent trees, the pulling should only run upstream " + "nodes _inside_ the scope of the macro, relying on the explicit input" + "value" + ) + + self.assertEqual( + (2 + 1) + 1 + 1, + macro.two.pull(run_parent_trees_too=True), + msg="Running with parent trees, the pulling should also run the parents " + "data dependencies first" + ) + + def test_recovery_after_failed_pull(self): + def grab_x_and_run(node): + """Grab a couple connections from an add_one-like node""" + return node.inputs.x.connections + node.signals.input.run.connections + + with self.subTest("When the local scope has cyclic data flow"): + def cyclic_macro(macro): + macro.one = SingleValue(add_one) + macro.two = SingleValue(add_one, x=macro.one) + macro.one.inputs.x = macro.two + macro.one > macro.two + macro.starting_nodes = [macro.one] + # We need to manually specify execution since the data flow is cyclic + + m = Macro(cyclic_macro) + + initial_labels = list(m.nodes.keys()) + + def grab_connections(macro): + return grab_x_and_run(macro.one) + grab_x_and_run(macro.two) + + initial_connections = grab_connections(m) + + with self.assertRaises( + CircularDataFlowError, + msg="Pull should only work for DAG workflows" + ): + m.two.pull() + self.assertListEqual( + initial_labels, + list(m.nodes.keys()), + msg="Labels should be restored after failing to pull because of acyclicity" + ) + self.assertTrue( + all(c is ic for (c, ic) in zip(grab_connections(m), initial_connections)), + msg="Connections should be restored after failing to pull because of " + "cyclic data flow" + ) + + with self.subTest("When the parent scope has cyclic data flow"): + n1 = SingleValue(add_one, label="n1", x=0) + n2 = SingleValue(add_one, label="n2", x=n1) + m = Macro(add_three_macro, label="m", one__x=n2) + + self.assertEqual( + 0 + 1 + 1 + (1 + 1 + 1), + m.three.pull(run_parent_trees_too=True), + msg="Sanity check, without cyclic data flows pulling here should be ok" + ) + + n1.inputs.x = n2 + + initial_connections = grab_x_and_run(n1) + grab_x_and_run(n2) + with self.assertRaises( + CircularDataFlowError, + msg="Once the outer scope has circular data flows, pulling should fail" + ): + m.three.pull(run_parent_trees_too=True) + self.assertTrue( + all( + c is ic + for (c, ic) in zip( + grab_x_and_run(n1) + grab_x_and_run(n2), initial_connections + ) + ), + msg="Connections should be restored after failing to pull because of " + "cyclic data flow in the outer scope" + ) + self.assertEqual( + "n1", + n1.label, + msg="Labels should get restored in the outer scope" + ) + self.assertEqual( + "one", + m.one.label, + msg="Labels should not have even gotten perturbed to start with in the" + "inner scope" + ) + + with self.subTest("When a node breaks upstream"): + def fail_at_zero(x): + y = 1 / x + return y + + n1 = SingleValue(fail_at_zero, x=0) + n2 = SingleValue(add_one, x=n1, label="n1") + n_not_used = SingleValue(add_one) + n_not_used > n2 # Just here to make sure it gets restored + + with self.assertRaises( + ZeroDivisionError, + msg="The underlying error should get raised" + ): + n2.pull() + self.assertEqual( + "n1", + n2.label, + msg="Original labels should get restored on upstream failure" + ) + self.assertIs( + n_not_used, + n2.signals.input.run.connections[0].node, + msg="Original connections should get restored on upstream failure" + ) + if __name__ == '__main__': unittest.main() diff --git a/tests/unit/test_workflow.py b/tests/unit/test_workflow.py index fc7b8775..537bb8ac 100644 --- a/tests/unit/test_workflow.py +++ b/tests/unit/test_workflow.py @@ -462,6 +462,42 @@ def test_io_label_maps_are_bijective(self): with self.assertRaises(ValueDuplicationError): wf.inputs_map["foo2__x"] = "x1" + def test_pull_and_executors(self): + def add_three_macro(macro): + macro.one = Workflow.create.SingleValue(plus_one) + macro.two = Workflow.create.SingleValue(plus_one, x=macro.one) + macro.three = Workflow.create.SingleValue(plus_one, x=macro.two) + + wf = Workflow("pulling") + + wf.n1 = Workflow.create.SingleValue(plus_one, x=0) + wf.m = Workflow.create.Macro(add_three_macro, one__x=wf.n1) + + self.assertEquals( + (0 + 1) + (1 + 1), + wf.m.two.pull(run_parent_trees_too=True), + msg="Sanity check, pulling here should work perfectly fine" + ) + + wf.m.one.executor = True + with self.assertRaises( + ValueError, + msg="Should not be able to pull with executor in local scope" + ): + wf.m.two.pull() + wf.m.one.executor = False + + wf.n1.executor = True + with self.assertRaises( + ValueError, + msg="Should not be able to pull with executor in parent scope" + ): + wf.m.two.pull(run_parent_trees_too=True) + + # Pulling in the local scope should be fine with an executor only in the parent + # scope + wf.m.two.pull(run_parent_trees_too=False) + if __name__ == '__main__': unittest.main()