diff --git a/notebooks/workflow_example.ipynb b/notebooks/workflow_example.ipynb index 9ed57c66e..e273a9812 100644 --- a/notebooks/workflow_example.ipynb +++ b/notebooks/workflow_example.ipynb @@ -11,7 +11,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "88c66e527673496c8f5b7ea75538baa0", + "model_id": "00c1cb12911741a18f9c06ba09e74ae6", "version_major": 2, "version_minor": 0 }, @@ -36,9 +36,9 @@ "- How to instantiate a node\n", "- How to make reusable node classes\n", "- How to connect node inputs and outputs together\n", + "- Flow control (i.e. signal channels vs data channels)\n", "- Defining new nodes from special node classes (Fast and SingleValue)\n", "- The five ways of adding nodes to a workflow\n", - "- Flow control (i.e. signal channels vs data channels)\n", "- Using pre-defined nodes " ] }, @@ -357,6 +357,43 @@ "adder_node.outputs.sum_.value" ] }, + { + "cell_type": "markdown", + "id": "c0997630-c053-42bb-8c0d-332f8bc26216", + "metadata": {}, + "source": [ + "Finally, when running (or updating or calling when those result in a run -- i.e. the node is set to run on updates and is ready) a function node returns the wrapped function output directly:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "69b59737-9e09-4b4b-a0e2-76a09de02c08", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "31" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "adder_node(15, 16)" + ] + }, + { + "cell_type": "markdown", + "id": "f233f3f7-9576-4400-8e92-a1f6109d7f9b", + "metadata": {}, + "source": [ + "Note for advanced users: when the node has an executor set, running returns a futures object for the calculation, whose `.result()` will eventually be the function output." + ] + }, { "cell_type": "markdown", "id": "07a22cee-e340-4551-bb81-07d8be1d152b", @@ -373,7 +410,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 14, "id": "61b43a9b-8dad-48b7-9194-2045e465793b", "metadata": {}, "outputs": [], @@ -383,7 +420,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 15, "id": "647360a9-c971-4272-995c-aa01e5f5bb83", "metadata": {}, "outputs": [ @@ -421,7 +458,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 16, "id": "8fb0671b-045a-4d71-9d35-f0beadc9cf3a", "metadata": {}, "outputs": [ @@ -431,7 +468,7 @@ "-10" ] }, - "execution_count": 15, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -452,7 +489,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 17, "id": "5ce91f42-7aec-492c-94fb-2320c971cd79", "metadata": {}, "outputs": [ @@ -488,7 +525,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 18, "id": "20360fe7-b422-4d78-9bd1-de233f28c8df", "metadata": {}, "outputs": [ @@ -521,7 +558,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 19, "id": "1a4e9693-0980-4435-aecc-3331d8b608dd", "metadata": {}, "outputs": [], @@ -533,7 +570,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 20, "id": "7c4d314b-33bb-4a67-bfb9-ed77fba3949c", "metadata": {}, "outputs": [ @@ -580,7 +617,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 21, "id": "2e418abf-7059-4e1e-9b9f-b3dc0a4b5e35", "metadata": {}, "outputs": [ @@ -621,7 +658,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 22, "id": "3310eac4-04f6-421b-9824-19bb2d680be6", "metadata": {}, "outputs": [ @@ -663,7 +700,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 23, "id": "7a6f2bce-6b5e-4321-9457-0a6790d2202a", "metadata": {}, "outputs": [], @@ -673,13 +710,13 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 24, "id": "6569014a-815b-46dd-8b47-4e1cd4584b3b", "metadata": {}, "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAiMAAAGdCAYAAADAAnMpAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAgdElEQVR4nO3db0yd9f3/8dc5h8KpHRxDK3Dasopdq0WiDhoqdI2Zs4xq8NsbSzGuVl1NpOpq7XRp00WkMSG6aKabZf6rxrQ6olMjCUO5sSltt7FSmojHRGPZaO1BQomH4x/a9ZzP70YDP88OUM4pnA/nnOcjOTe4+Jz2zZVu5+l1cT7HYYwxAgAAsMRpewAAAJDeiBEAAGAVMQIAAKwiRgAAgFXECAAAsIoYAQAAVhEjAADAKmIEAABYlWF7gKkIh8M6efKksrOz5XA4bI8DAACmwBijYDCohQsXyumc+PpHUsTIyZMnVVhYaHsMAAAQh+PHj2vx4sUTfj8pYiQ7O1vSuR8mJyfH8jQAAGAqhoeHVVhYOPY6PpGkiJHRWzM5OTnECAAASeZ8v2LBL7ACAACriBEAAGAVMQIAAKwiRgAAgFXECAAAsIoYAQAAVhEjAADAKmIEAABYlRSbngHAbBAKG3X2DmkgOKK8bLfKi3LlcvJ5WcCFIkYAYAraevxqaPHJHxgZO+b1uFVfU6zqEq/FyYDkx20aADiPth6/tuw7EhEiktQfGNGWfUfU1uO3NBmQGogRAJhEKGzU0OKTGed7o8caWnwKhcdbAWAqiBEAmERn71DUFZHvMpL8gRF19g4lbiggxRAjADCJgeDEIRLPOgDRiBEAmERetnta1wGIRowAwCTKi3Ll9bg10Rt4HTr3rpryotxEjgWkFGIEACbhcjpUX1MsSVFBMvp1fU0x+40AF4AYAYDzqC7xqmljqQo8kbdiCjxuNW0sZZ8R4AKx6RkATEF1iVdriwvYgRWYAcQIAEyRy+lQxdL5tscAUg63aQAAgFXECAAAsIoYAQAAVhEjAADAKmIEAABYRYwAAACriBEAAGAVMQIAAKwiRgAAgFXECAAAsIoYAQAAVhEjAADAKmIEAABYRYwAAACriBEAAGAVMQIAAKwiRgAAgFXECAAAsIoYAQAAVhEjAADAKmIEAABYRYwAAACriBEAAGAVMQIAAKwiRgAAgFXECAAAsIoYAQAAVhEjAADAKmIEAABYRYwAAACriBEAAGAVMQIAAKwiRgAAgFXECAAAsIoYAQAAVmXYHgAAgOkUCht19g5pIDiivGy3yoty5XI6bI+FSRAjAICU0dbjV0OLT/7AyNgxr8et+ppiVZd4LU6GyXCbBgCQEtp6/Nqy70hEiEhSf2BEW/YdUVuP39JkOB9iBACQ9EJho4YWn8w43xs91tDiUyg83grYRowAAJJeZ+9Q1BWR7zKS/IERdfYOJW4oTBkxAgBIegPBiUMknnVILGIEAJD08rLd07oOiUWMAACSXnlRrrwetyZ6A69D595VU16Um8ixMEXECAAg6bmcDtXXFEtSVJCMfl1fU8x+I7MUMQIASAnVJV41bSxVgSfyVkyBx62mjaXsMzKLsekZACBlVJd4tba4gB1Yk0xcV0b27NmjoqIiud1ulZWVqaOjY9L1+/fv19VXX62LLrpIXq9Xd955p06dOhXXwAAATMbldKhi6Xz93zWLVLF0PiGSBGKOkebmZm3btk27du1Sd3e31qxZo3Xr1qmvr2/c9QcOHNCmTZu0efNmffTRR3r99df1r3/9S3fdddcFDw8AAJJfzDHy5JNPavPmzbrrrru0YsUK/e53v1NhYaGamprGXf+Pf/xDl156qbZu3aqioiL96Ec/0t13363Dhw9f8PAAACD5xRQjZ86cUVdXl6qqqiKOV1VV6dChQ+M+p7KyUidOnFBra6uMMfriiy/0xhtv6Kabbprw7zl9+rSGh4cjHgAAIDXFFCODg4MKhULKz8+POJ6fn6/+/v5xn1NZWan9+/ertrZWmZmZKigo0MUXX6zf//73E/49jY2N8ng8Y4/CwsJYxgQAAEkkrl9gdTgifxnIGBN1bJTP59PWrVv18MMPq6urS21tbert7VVdXd2Ef/7OnTsVCATGHsePH49nTAAAkARiemvvggUL5HK5oq6CDAwMRF0tGdXY2KjVq1froYcekiRdddVVmjdvntasWaNHH31UXm/0+76zsrKUlZUVy2gAACBJxXRlJDMzU2VlZWpvb4843t7ersrKynGf880338jpjPxrXC6XpHNXVAAAQHqL+TbN9u3b9cILL2jv3r36+OOP9cADD6ivr2/stsvOnTu1adOmsfU1NTV688031dTUpGPHjungwYPaunWrysvLtXDhwun7SQAAQFKKeQfW2tpa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hUurGMnLdk/rOgAAcOHSKkbKi3Ll9bg10Rt4HTr3rpryotxEjgUAQFpLqxhxOR2qrymWpKggGf26vqaY/UYAAEigtIoRSaou8appY6kKPJG3Ygo8bjVtLGWfEQAAEiwtNz2rLvFqbXEBO7ACADALpGWMSOdu2VQsnW97DAAA0l7a3aYBAACzCzECAACsIkYAAIBVxAgAALCKGAEAAFYRIwAAwCpiBAAAWEWMAAAAq4gRAABgVVLswGqMkSQNDw9bngQAAEzV6Ov26Ov4RJIiRoLBoCSpsLDQ8iQAACBWwWBQHo9nwu87zPlyZRYIh8M6efKksrOz5XBEfpjd8PCwCgsLdfz4ceXk5FiaML1wzhOPc24H5z3xOOd2zNR5N8YoGAxq4cKFcjon/s2QpLgy4nQ6tXjx4knX5OTk8A83wTjnicc5t4Pznniccztm4rxPdkVkFL/ACgAArCJGAACAVUkfI1lZWaqvr1dWVpbtUdIG5zzxOOd2cN4Tj3Nuh+3znhS/wAoAAFJX0l8ZAQAAyY0YAQAAVhEjAADAKmIEAABYNetjZM+ePSoqKpLb7VZZWZk6OjomXf/++++rrKxMbrdbl112mf74xz8maNLUEst5f/PNN7V27VpdcsklysnJUUVFhd59990ETpsaYv23PurgwYPKyMjQNddcM7MDpqhYz/vp06e1a9cuLVmyRFlZWVq6dKn27t2boGlTQ6znfP/+/br66qt10UUXyev16s4779SpU6cSNG3y++CDD1RTU6OFCxfK4XDo7bffPu9zEv5aamaxP/3pT2bOnDnm+eefNz6fz9x///1m3rx55j//+c+4648dO2Yuuugic//99xufz2eef/55M2fOHPPGG28kePLkFut5v//++81jjz1mOjs7zSeffGJ27txp5syZY44cOZLgyZNXrOd81Jdffmkuu+wyU1VVZa6++urEDJtC4jnvN998s1m1apVpb283vb295p///Kc5ePBgAqdObrGe846ODuN0Os1TTz1ljh07Zjo6OsyVV15p1q9fn+DJk1dra6vZtWuX+fOf/2wkmbfeemvS9TZeS2d1jJSXl5u6urqIY1dccYXZsWPHuOt//etfmyuuuCLi2N13322uvfbaGZsxFcV63sdTXFxsGhoapnu0lBXvOa+trTW/+c1vTH19PTESh1jP+1/+8hfj8XjMqVOnEjFeSor1nP/2t781l112WcSxp59+2ixevHjGZkxlU4kRG6+ls/Y2zZkzZ9TV1aWqqqqI41VVVTp06NC4z/n73/8etf6nP/2pDh8+rP/+978zNmsqiee8/69wOKxgMKjc3NyZGDHlxHvOX3rpJX322Weqr6+f6RFTUjzn/Z133tHKlSv1+OOPa9GiRVq+fLkefPBBffvtt4kYOenFc84rKyt14sQJtba2yhijL774Qm+88YZuuummRIyclmy8ls7aD8obHBxUKBRSfn5+xPH8/Hz19/eP+5z+/v5x1589e1aDg4Pyer0zNm+qiOe8/68nnnhCX3/9tTZs2DATI6aceM75p59+qh07dqijo0MZGbP2f8azWjzn/dixYzpw4IDcbrfeeustDQ4O6p577tHQ0BC/NzIF8ZzzyspK7d+/X7W1tRoZGdHZs2d188036/e//30iRk5LNl5LZ+2VkVEOhyPia2NM1LHzrR/vOCYX63kf9dprr+mRRx5Rc3Oz8vLyZmq8lDTVcx4KhXTrrbeqoaFBy5cvT9R4KSuWf+vhcFgOh0P79+9XeXm5brzxRj355JN6+eWXuToSg1jOuc/n09atW/Xwww+rq6tLbW1t6u3tVV1dXSJGTVuJfi2dtf9JtWDBArlcrqhaHhgYiCq2UQUFBeOuz8jI0Pz582ds1lQSz3kf1dzcrM2bN+v111/XDTfcMJNjppRYz3kwGNThw4fV3d2t++67T9K5F0ljjDIyMvTee+/p+uuvT8jsySyef+ter1eLFi2K+Ej0FStWyBijEydOaNmyZTM6c7KL55w3NjZq9erVeuihhyRJV111lebNm6c1a9bo0Ucf5Yr3DLDxWjprr4xkZmaqrKxM7e3tEcfb29tVWVk57nMqKiqi1r/33ntauXKl5syZM2OzppJ4zrt07orIHXfcoVdffZV7uTGK9Zzn5OToww8/1NGjR8cedXV1uvzyy3X06FGtWrUqUaMntXj+ra9evVonT57UV199NXbsk08+kdPp1OLFi2d03lQQzzn/5ptv5HRGvlS5XC5J//+/1jG9rLyWztivxk6D0beAvfjii8bn85lt27aZefPmmX//+9/GGGN27NhhbrvttrH1o29HeuCBB4zP5zMvvvgib+2NQ6zn/dVXXzUZGRnmmWeeMX6/f+zx5Zdf2voRkk6s5/x/8W6a+MR63oPBoFm8eLH52c9+Zj766CPz/vvvm2XLlpm77rrL1o+QdGI95y+99JLJyMgwe/bsMZ999pk5cOCAWblypSkvL7f1IySdYDBouru7TXd3t5FknnzySdPd3T32durZ8Fo6q2PEGGOeeeYZs2TJEpOZmWlKS0vN+++/P/a922+/3Vx33XUR6//2t7+ZH/7whyYzM9NceumlpqmpKcETp4ZYzvt1111nJEU9br/99sQPnsRi/bf+XcRI/GI97x9//LG54YYbzNy5c83ixYvN9u3bzTfffJPgqZNbrOf86aefNsXFxWbu3LnG6/Wan//85+bEiRMJnjp5/fWvf530/6Nnw2upwxiucwEAAHtm7e+MAACA9ECMAAAAq4gRAABgFTECAACsIkYAAIBVxAgAALCKGAEAAFYRIwAAwCpiBAAAWEWMAAAAq4gRAABgFTECAACs+n8+Ow+kNMUfdwAAAABJRU5ErkJggg==", + "image/png": "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xrqA16GzV9UUZKWGrSbCCAAAMSQ+zqGKkmxJ8gskw88rSrLD2m+EMAIAQIxZkeNWzZpcZbh8D8VkuJyqWZMb9j4jND0DACAGrchxa3l2Bh1YAQCAPfFxDhXOm267DA7TAAAAuwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsSbBcAAOdjcMioua1HnX39Skt2qiArVfFxDttlAQgAYQRAxGo45lFlXas83v6RMbfLqYqSbK3IcVusDEAgOEwDICI1HPNo/c7DPkFEkjq8/Vq/87AajnksVQYgUIQRABFncMiosq5VZpSfDY9V1rVqcGi0GQAmG8IIgIjT3Nbjt0fkXEaSx9uv5rae8BUFIGiEEQARp7Nv7CASzDwAdhFGAESctGTnhM4DYBdhBEDEKchKldvl1FgX8Dp09qqagqzUcJYFIEiEEQARJz7OoYqSbEnyCyTDzytKsuk3AkQIwgiAiLQix62aNbnKcPkeislwOVWzJpc+I0AEoekZgIi1Iset5dkZdGAFIhxhBEBEi49zqHDedNtlADgPHKYBAABWBRVGqqurlZWVJafTqby8PDU1NX3n/IGBAW3ZskVz5sxRUlKS5s2bpx07dgRVMAAAiC4BH6apra3Vxo0bVV1draVLl+rZZ5/VypUr1draqosvvnjUbVavXq3PPvtM27dv1w9/+EN1dnbqzJkz5108AACIfA5jTEA3b1iyZIlyc3NVU1MzMrZw4UKtWrVKVVVVfvMbGhp044036vjx40pNDe6a/97eXrlcLnm9XqWkpAT1OwAAQHiN9/s7oMM0p0+fVktLi4qLi33Gi4uLdeDAgVG3efPNN5Wfn6+HH35Ys2bN0oIFC3Tffffp66+/HvN1BgYG1Nvb6/MAAADRKaDDNF1dXRocHFR6errPeHp6ujo6Okbd5vjx49q3b5+cTqdef/11dXV16c4771RPT8+Y541UVVWpsrIykNIAAECECuoEVofD9xp+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39bVV18dltqjVTB/I263W7NmzfK51frChQtljNHJkyc1f/78kNYczYJZj6qqKi1dulT333+/JOnyyy/XtGnTtGzZMj344IPsXQ+BSb9nJDExUXl5eWpsbPQZb2xsVFFR0ajbFBYW+s1/++23lZ+frylTpoSs1lgQzHpIZ/eI3HrrrXrllVc47jqBAl2PlJQUvf/++zp69OjIo6ysTD/60Y909OhRLVmyJFylR61g/kaWLl2qU6dO6YsvvhgZ++ijjxQXF6fZs2eHtN5oF8x6fPXVV4qL8/16jI+Pl/T/97Jjgtk6czYQw5dlbd++3bS2tpqNGzeaadOmmf/+97/GGGM2b95sbr755pH5w5f23nvvvaa1tdVs376dS3snUKDr8corr5iEhATz9NNPG4/HM/L4/PPPbb2FqBLoenwbV9NMvEDXpK+vz8yePdv88pe/NB988IHZu3evmT9/vrn99tttvYWoEuh6vPjiiyYhIcFUV1ebTz75xOzbt8/k5+ebgoICW28h6kVEGDHGmKefftrMmTPHJCYmmtzcXLN3796Rn91yyy3mqquu8pn/7rvvmsWLF5vExERzySWXmJqamjBXHN0CWY+rrrrKSPJ73HLLLeEvPEoF+vdxLsJIaAS6Jh9++KG55pprzNSpU83s2bPNpk2bzFdffRXmqqNXoOvxxBNPmOzsbDN16lTjdrvNr371K3Py5MkwVx07HMawzwkAANgz6c8ZAQAA0Y0wAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwKr/Bwq4+PKt2NvcAAAAAElFTkSuQmCC", "text/plain": [ "
" ] @@ -719,7 +756,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 25, "id": "25f0495a-e85f-43b7-8a70-a2c9cbd51ebb", "metadata": {}, "outputs": [ @@ -729,7 +766,7 @@ "(False, False)" ] }, - "execution_count": 24, + "execution_count": 25, "metadata": {}, "output_type": "execute_result" } @@ -740,7 +777,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 26, "id": "449ce797-be62-4211-b483-c717a3d70583", "metadata": {}, "outputs": [ @@ -750,7 +787,7 @@ "(True, False)" ] }, - "execution_count": 25, + "execution_count": 26, "metadata": {}, "output_type": "execute_result" } @@ -762,13 +799,13 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 27, "id": "7008b0fc-3644-401c-b49f-9c40f9d89ac4", "metadata": {}, "outputs": [ { "data": { - "image/png": "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nk0DAABSIiX7jAAAACw2wggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsCrXdgEAsBgiM0Z9gxManZpWcYFPNeVF8uZ4bJcFYAEIIwAyXvdASC2dQYXC03PnAn6fmhsqtKUyYLEyAAvBMA2AjNY9ENLOI2digogkjYSntfPIGXUPhCxVBmChCCMAMlZkxqilMyiT4GfRcy2dQUVmErUAsFQQRgBkrL7Bibg7IlczkkLhafUNTqSvKACOEUYAZKzRqfmDyNWO//f/5e4IsIQRRgBkrOIC34Laffdn5/WZJ/+d+SPAEkUYAZCxasqLFPD7tJAFvExoBZYuwgiAjOXN8ai5oUKSrhtImNAKLF2EEQAZbUtlQO3bq1Tqv/6QDRNagaWJMAIg422pDOjYN+7UI//nowtqv9CJrwDSgzACICt4czza+NGVC2q70ImvANKDMAIga1xvQqtHs9vE15QXpbMsANdBGAGQNa41oTX6urmhggfoAUsMYQRAVplvQmup36f27VU8OA9YgnhqL4Css6UyoM0VpeobnNDo1LSKC2aHZrgjAixNhBEAWcmb41Ht2hW2ywCwAAzTAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsSiqMtLW1qby8XD6fT9XV1ert7V3Q+44fP67c3FzdcccdyfxaAACQhRyHkY6ODu3evVsHDhxQf3+/Nm3apK1bt2poaOia7wuHw9qxY4f+/M//POliAQBA9vEYY4yTN2zYsEFVVVVqb2+fO7du3Tpt27ZNra2t877vi1/8om699VZ5vV79+Mc/1tmzZxf8OycnJ+X3+xUOh1VYWOikXAAAYMlCr9+O7oxcvnxZp0+fVn19fcz5+vp6nThxYt73vfDCCzp//ryam5sX9HsuXbqkycnJmAMAAGQnR2FkbGxMkUhEJSUlMedL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6/Tp0+ru7tbg4KCamprSUaprpes6mhFfo1auXCmv1xuXmEdHR+NSW1RpaWnC9rm5uVqxYkXKas0myfR7VEdHhx566CG9/PLLuuuuu1JZZlZx2udTU1M6deqU+vv79cgjj0iavVAaY5Sbm6s33nhDd955Z1pqz1TJ/DsPBAK6+eab5ff7586tW7dOxhhdvHhRt956a0prznTJ9Hlra6s2btyoxx57TJL0yU9+UsuXL9emTZv0xBNPcLc7BdJ5Hc2IOyN5eXmqrq5WT09PzPmenh7V1dUlfE9tbW1c+zfeeEPr16/XsmXLUlZrNkmm36XZOyIPPPCAXnzxRcZzHXLa54WFhfrlL3+ps2fPzh1NTU362Mc+prNnz2rDhg3pKj1jJfPvfOPGjXrnnXf0+9//fu7c22+/rZycHK1evTql9WaDZPr8vffeU05O7CXL6/VK+v/f1rG40nodXfQpsSkSXQZ26NAhEwwGze7du83y5cvNb37zG2OMMfv27TP333//XPvokqQ9e/aYYDBoDh06xNLeJDjt9xdffNHk5uaaZ5991oRCobnjd7/7na0/IeM47fP3YzWNc077fGpqyqxevdr81V/9lfnVr35l3nzzTXPrrbeahx9+2NafkHGc9vkLL7xgcnNzTVtbmzl//rw5duyYWb9+vampqbH1J2Scqakp09/fb/r7+40k8/TTT5v+/v655dQ2r6MZE0aMMebZZ581a9asMXl5eaaqqsq8+eabcz/78pe/bD772c/GtP+P//gP86lPfcrk5eWZj3zkI6a9vT3NFWcHJ/3+2c9+1kiKO7785S+nv/AM5vTf+tUII8lx2ufnzp0zd911l7nhhhvM6tWrzd69e817772X5qozm9M+f+aZZ0xFRYW54YYbTCAQMF/60pfMxYsX01x15vrZz352zf8/27yOeozh/hYAALAnI+aMAACA7EUYAQAAVhFGAACAVYQRAABgFWEEAABYRRgBAABWEUYAAIBVhBEAAGAVYQQAAFhFGAEAAFYRRgAAgFWEEQAAYNX/A3IrpT43izeiAAAAAElFTkSuQmCC", + "image/png": "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dOTIEe3du1cHDhyI7zsBAMxZZoZHFauij04DieY4jFRXV2t4eFg7d+5UMBhUcXGx2tratGLFCklSMBiM2HMkEAiora1Njz32mJ577jktW7ZMzzzzjL72ta/F710AAADX8pip1aRJbHR0VD6fT6FQiPUjAAC4xFyv3zybBgAAWEUYAQAAVhFGAACAVYQRAABgFWEEAABYRRgBAABWEUYAAIBVjjc9s2FqK5TR0VHLlQAAgLmaum5faUszV4SRsbExSVJBQYHlSgAAgFNjY2Py+Xyzft8VO7BOTk7q3LlzWrRokTye6A9yGh0dVUFBgU6fPs0urQnGubeHc28P594ezr0dsZx3Y4zGxsa0bNkyZWTMvjLEFSMjGRkZWr58+Zz65uTk8MtpCefeHs69PZx7ezj3djg975cbEZnCAlYAAGAVYQQAAFiVMmEkOztb9fX1ys7Otl1K2uHc28O5t4dzbw/n3o75PO+uWMAKAABSV8qMjAAAAHcijAAAAKsIIwAAwCrCCAAAsMpVYaSpqUmBQEBer1elpaXq7Oy8bP+DBw+qtLRUXq9XK1eu1J49exJUaepxcu5ff/11rVu3Tp/5zGeUk5OjiooK/frXv05gtanD6e/8lHfeeUdZWVm65ZZb5rfAFOb03E9MTGjHjh1asWKFsrOztWrVKr3wwgsJqja1OD33+/fv180336xrr71Wfr9fDz/8sIaHhxNUbep4++23VVVVpWXLlsnj8egXv/jFFY+J23XWuMS//Mu/mAULFpif/vSnpre31zz66KPmuuuuM//3f/8Xtf+pU6fMtddeax599FHT29trfvrTn5oFCxaYV199NcGVu5/Tc//oo4+ap59+2nR1dZn33nvP1NXVmQULFphjx44luHJ3c3rep/zhD38wK1euNJWVlebmm29OTLEpJpZzf88995jbbrvNdHR0mL6+PvOf//mf5p133klg1anB6bnv7Ow0GRkZ5kc/+pE5deqU6ezsNJ///OfNvffem+DK3a+trc3s2LHDvPbaa0aSeeONNy7bP57XWdeEkfLyclNTUxPR9rnPfc488cQTUfv//d//vfnc5z4X0fbII4+YL37xi/NWY6pyeu6jKSoqMg0NDfEuLaXFet6rq6vNP/7jP5r6+nrCSIycnvt/+7d/Mz6fzwwPDyeivJTm9Nz/8z//s1m5cmVE2zPPPGOWL18+bzWmg7mEkXheZ10xTXP+/Hl1d3ersrIyor2yslKHDx+OesyRI0dm9L/zzjt19OhRffzxx/NWa6qJ5dx/2uTkpMbGxpSbmzsfJaakWM/7iy++qJMnT6q+vn6+S0xZsZz7X/3qVyorK9P3v/99XX/99brpppv0ne98R3/84x8TUXLKiOXcr169WmfOnFFbW5uMMXr//ff16quv6u67705EyWktntdZVzwob2hoSOFwWHl5eRHteXl5GhgYiHrMwMBA1P4XLlzQ0NCQ/H7/vNWbSmI595/2wx/+UB9++KHuu++++SgxJcVy3n//+9/riSeeUGdnp7KyXPFPOynFcu5PnTqlQ4cOyev16o033tDQ0JC++c1vamRkhHUjDsRy7levXq39+/erurpa4+PjunDhgu655x79+Mc/TkTJaS2e11lXjIxM8Xg8EV8bY2a0Xal/tHZcmdNzP+XAgQP63ve+p5aWFi1dunS+yktZcz3v4XBY999/vxoaGnTTTTclqryU5uR3fnJyUh6PR/v371d5ebnuuusu7dq1Sy+99BKjIzFwcu57e3u1bds2Pfnkk+ru7lZ7e7v6+vpUU1OTiFLTXryus674+LRkyRJlZmbOSMaDg4MzUtmU/Pz8qP2zsrK0ePHieas11cRy7qe0tLRo8+bN+vnPf6477rhjPstMOU7P+9jYmI4ePaqenh59+9vflnTxAmmMUVZWlt58803dfvvtCand7WL5nff7/br++usjHpVeWFgoY4zOnDmjG2+8cV5rThWxnPvGxkatWbNGjz/+uCTpC1/4gq677jqtXbtWTz31FKPg8yie11lXjIwsXLhQpaWl6ujoiGjv6OjQ6tWrox5TUVExo/+bb76psrIyLViwYN5qTTWxnHvp4ojIQw89pFdeeYW52xg4Pe85OTn67W9/q+PHj0+/ampq9Od//uc6fvy4brvttkSV7nqx/M6vWbNG586d0wcffDDd9t577ykjI0PLly+f13pTSSzn/qOPPlJGRuSlLDMzU9Inn9IxP+J6nXW85NWSqdu99u7da3p7e8327dvNddddZ/73f//XGGPME088YR544IHp/lO3HD322GOmt7fX7N27l1t7Y+T03L/yyismKyvLPPfccyYYDE6//vCHP9h6C67k9Lx/GnfTxM7puR8bGzPLly83f/VXf2V+97vfmYMHD5obb7zRbNmyxdZbcC2n5/7FF180WVlZpqmpyZw8edIcOnTIlJWVmfLycltvwbXGxsZMT0+P6enpMZLMrl27TE9Pz/Rt1fN5nXVNGDHGmOeee86sWLHCLFy40JSUlJiDBw9Of+/BBx80X/7ylyP6/+Y3vzG33nqrWbhwofnsZz9rmpubE1xx6nBy7r/85S8bSTNeDz74YOILdzmnv/OXIoxcHafn/t133zV33HGHueaaa8zy5ctNbW2t+eijjxJcdWpweu6feeYZU1RUZK655hrj9/vN3/zN35gzZ84kuGr3+/d///fL/t89n9dZjzGMYwEAAHtcsWYEAACkLsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAq/4foA0IgyVTI5cAAAAASUVORK5CYII=", "text/plain": [ "
" ] @@ -806,7 +843,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 28, "id": "1cd000bd-9b24-4c39-9cac-70a3291d0660", "metadata": {}, "outputs": [], @@ -831,7 +868,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 29, "id": "7964df3c-55af-4c25-afc5-9e07accb606a", "metadata": {}, "outputs": [ @@ -872,7 +909,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 30, "id": "809178a5-2e6b-471d-89ef-0797db47c5ad", "metadata": {}, "outputs": [ @@ -913,22 +950,15 @@ "id": "18ba07ca-f1f9-4f05-98db-d5612f9acbb6", "metadata": {}, "source": [ - "Unlike function nodes, workflow input has no intrinsic order. We can still update it by calling the workflow, but we _need_ to use keyword and not positional arguments:" + "Unlike function nodes, workflow input has no intrinsic order. We can still update it by calling the workflow, but we _need_ to use keyword and not positional arguments. Runs of the workflow (which typically happen when the workflow is updated or called) return a dot-accessible dictionary based on the output channels:" ] }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 31, "id": "52c48d19-10a2-4c48-ae81-eceea4129a60", "metadata": {}, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "7\n" - ] - }, { "name": "stderr", "output_type": "stream", @@ -938,11 +968,42 @@ "/Users/huber/work/pyiron/pyiron_contrib/pyiron_contrib/workflow/io.py:80: UserWarning: Assigning a channel with the label x to the io key b_x\n", " warn(\n" ] + }, + { + "data": { + "text/plain": [ + "{'sum_sum_': 7}" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "wf(a_x=2, b_x=3)\n", - "print(wf.outputs.sum_sum_.value)" + "out = wf(a_x=2, b_x=3)\n", + "out" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "bb35ba3e-602d-4c9c-b046-32da9401dd1c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "7" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "out.sum_sum_" ] }, { @@ -969,7 +1030,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 33, "id": "ae500d5e-e55b-432c-8b5f-d5892193cdf5", "metadata": {}, "outputs": [ @@ -977,9 +1038,9 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_contrib/pyiron_contrib/workflow/composite.py:193: UserWarning: Reassigning the node bulk_structure to the label structure when adding it to the parent with_prebuilt.\n", + "/Users/huber/work/pyiron/pyiron_contrib/pyiron_contrib/workflow/composite.py:224: UserWarning: Reassigning the node bulk_structure to the label structure when adding it to the parent with_prebuilt.\n", " warn(\n", - "/Users/huber/work/pyiron/pyiron_contrib/pyiron_contrib/workflow/composite.py:193: UserWarning: Reassigning the node lammps to the label engine when adding it to the parent with_prebuilt.\n", + "/Users/huber/work/pyiron/pyiron_contrib/pyiron_contrib/workflow/composite.py:224: UserWarning: Reassigning the node lammps to the label engine when adding it to the parent with_prebuilt.\n", " warn(\n" ] }, @@ -994,9 +1055,9 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_contrib/pyiron_contrib/workflow/composite.py:193: UserWarning: Reassigning the node calc_md to the label calc when adding it to the parent with_prebuilt.\n", + "/Users/huber/work/pyiron/pyiron_contrib/pyiron_contrib/workflow/composite.py:224: UserWarning: Reassigning the node calc_md to the label calc when adding it to the parent with_prebuilt.\n", " warn(\n", - "/Users/huber/work/pyiron/pyiron_contrib/pyiron_contrib/workflow/composite.py:193: UserWarning: Reassigning the node scatter to the label plot when adding it to the parent with_prebuilt.\n", + "/Users/huber/work/pyiron/pyiron_contrib/pyiron_contrib/workflow/composite.py:224: UserWarning: Reassigning the node scatter to the label plot when adding it to the parent with_prebuilt.\n", " warn(\n" ] }, diff --git a/pyiron_contrib/workflow/composite.py b/pyiron_contrib/workflow/composite.py index bacc934e8..c27b3210f 100644 --- a/pyiron_contrib/workflow/composite.py +++ b/pyiron_contrib/workflow/composite.py @@ -57,6 +57,9 @@ class Composite(Node, ABC): By default, `run()` will be called on all owned nodes have output connections but no input connections (i.e. the upstream-most nodes), but this can be overridden to specify particular nodes to use instead. + The `run()` method (and `update()`, and calling the workflow, when these result in + a run), 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. @@ -92,15 +95,33 @@ def __init__( label: str, *args, parent: Optional[Composite] = None, + run_on_updates: bool = True, strict_naming: bool = True, **kwargs, ): - super().__init__(*args, label=label, parent=parent, **kwargs) + super().__init__( + *args, + label=label, + parent=parent, + run_on_updates=run_on_updates, + **kwargs + ) self.strict_naming: bool = strict_naming self.nodes: DotDict[str:Node] = DotDict() self.add: NodeAdder = NodeAdder(self) self.starting_nodes: None | list[Node] = None + @property + def executor(self) -> None: + return None + + @executor.setter + def executor(self, new_executor): + if new_executor is not None: + raise NotImplementedError( + "Running composite nodes with an executor is not yet supported" + ) + def to_dict(self): return { "label": self.label, @@ -115,12 +136,22 @@ def upstream_nodes(self) -> list[Node]: if node.outputs.connected and not node.inputs.connected ] + @property def on_run(self): + return self.run_graph + + @staticmethod + def run_graph(self): starting_nodes = ( self.upstream_nodes if self.starting_nodes is None else self.starting_nodes ) for node in starting_nodes: node.run() + return DotDict(self.outputs.to_value_dict()) + + @property + def run_args(self) -> dict: + return {"self": self} def add_node(self, node: Node, label: Optional[str] = None) -> None: """ diff --git a/pyiron_contrib/workflow/function.py b/pyiron_contrib/workflow/function.py index 883bae4bf..decc53a59 100644 --- a/pyiron_contrib/workflow/function.py +++ b/pyiron_contrib/workflow/function.py @@ -70,6 +70,10 @@ class Function(Node): on call. This invokes an `update()` call, which can in turn invoke `run()` if `run_on_updates` is set to `True`. + `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 `None` if it is not set to run on updates or not ready to run. Args: node_function (callable): The function determining the behaviour of the node. @@ -163,10 +167,12 @@ class Function(Node): {'p1': 2, 'm1': 1} Input data can be provided to both initialization and on call as ordered args - or keyword kwargs, e.g.: + 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: >>> plus_minus_1(2, y=3) - >>> plus_minus_1.outputs.to_value_dict() - {'p1': 3, 'm1': 2} + (3, 2) Finally, we might stop these updates from happening automatically, even when all the input data is present and available: @@ -180,8 +186,7 @@ class Function(Node): With these flags set, the node requires us to manually call a run: >>> plus_minus_1.run() - >>> plus_minus_1.outputs.to_value_dict() - {'p1': 1, 'm1': -1} + (-1, 1) So function nodes have the most basic level of protection that they won't run if they haven't seen any input data. @@ -360,6 +365,7 @@ def __init__( super().__init__( label=label if label is not None else node_function.__name__, parent=parent, + run_on_updates=run_on_updates, # **kwargs, ) @@ -379,7 +385,6 @@ def __init__( ) self._verify_that_channels_requiring_update_all_exist() - self.run_on_updates = run_on_updates self._batch_update_input(*args, **kwargs) if update_on_instantiation: @@ -535,6 +540,12 @@ def on_run(self): def run_args(self) -> dict: kwargs = self.inputs.to_value_dict() if "self" in self._input_args: + if self.executor is not None: + raise NotImplementedError( + f"The node {self.label} cannot be run on an executor because it " + f"uses the `self` argument and this functionality is not yet " + f"implemented" + ) kwargs["self"] = self return kwargs diff --git a/pyiron_contrib/workflow/node.py b/pyiron_contrib/workflow/node.py index 0e46dbce7..e5f2ec3d7 100644 --- a/pyiron_contrib/workflow/node.py +++ b/pyiron_contrib/workflow/node.py @@ -8,7 +8,7 @@ import warnings from abc import ABC, abstractmethod from concurrent.futures import Future -from typing import Optional, TYPE_CHECKING +from typing import Any, Optional, TYPE_CHECKING from pyiron_contrib.executors import CloudpickleProcessPoolExecutor from pyiron_contrib.workflow.files import DirectoryObject @@ -45,6 +45,16 @@ class Node(HasToDict, ABC): By default, nodes' signals input comes with `run` and `ran` IO ports which force the `run()` method and which emit after `finish_run()` is completed, respectfully. + 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` and has `run_on_updates = True`. + + 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 `update()`. + 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). + Nodes have a status, which is currently represented by the `running` and `failed` boolean flags. Their value is controlled automatically in the defined `run` and `finish_run` @@ -154,7 +164,7 @@ def outputs(self) -> Outputs: @property @abstractmethod - def on_run(self) -> callable[..., tuple]: + def on_run(self) -> callable[..., Any | tuple]: """ What the node actually does! """ @@ -167,7 +177,7 @@ def run_args(self) -> dict: """ return {} - def process_run_result(self, run_output: tuple) -> None: + def process_run_result(self, run_output: Any | tuple) -> None: """ What to _do_ with the results of `on_run` once you have them. @@ -176,7 +186,7 @@ def process_run_result(self, run_output: tuple) -> None: """ pass - def run(self) -> None: + def run(self) -> Any | tuple | Future: """ Executes the functionality of the node defined in `on_run`. Handles the status of the node, and communicating with any remote @@ -195,10 +205,11 @@ def run(self) -> None: self.running = False self.failed = True raise e - self.finish_run(run_output) + return self.finish_run(run_output) elif isinstance(self.executor, CloudpickleProcessPoolExecutor): self.future = self.executor.submit(self.on_run, **self.run_args) self.future.add_done_callback(self.finish_run) + return self.future else: raise NotImplementedError( "We currently only support executing the node functionality right on " @@ -206,7 +217,7 @@ def run(self) -> None: "pyiron_contrib.workflow.util.CloudpickleProcessPoolExecutor." ) - def finish_run(self, run_output: tuple | Future): + def finish_run(self, run_output: tuple | Future) -> Any | tuple: """ Switch the node status, process the run result, then fire the ran signal. @@ -224,6 +235,7 @@ def finish_run(self, run_output: tuple | Future): try: self.process_run_result(run_output) self.signals.output.ran() + return run_output except Exception as e: self.failed = True raise e @@ -234,9 +246,9 @@ def _build_signal_channels(self) -> Signals: signals.output.ran = OutputSignal("ran", self) return signals - def update(self) -> None: + def update(self) -> Any | tuple | Future | None: if self.run_on_updates and self.ready: - self.run() + return self.run() @property def working_directory(self): @@ -300,4 +312,4 @@ def _batch_update_input(self, **kwargs): def __call__(self, **kwargs) -> None: self._batch_update_input(**kwargs) - self.update() + return self.update() diff --git a/pyiron_contrib/workflow/workflow.py b/pyiron_contrib/workflow/workflow.py index 73d18f648..e2b543e80 100644 --- a/pyiron_contrib/workflow/workflow.py +++ b/pyiron_contrib/workflow/workflow.py @@ -88,8 +88,13 @@ class Workflow(Composite): These input keys can be used when calling the workflow to update the input. In our example, the nodes update automatically when their input gets updated, so all we need to do to see updated workflow output is update the input: - >>> wf(first_x=10) - >>> wf.outputs.second_y.value + >>> out = wf(first_x=10) + >>> out + {'second_y': 12} + + Note: this _looks_ like a dictionary, but has some extra convenience that we + can dot-access data: + >>> out.second_y 12 Workflows also give access to packages of pre-built nodes under different @@ -125,8 +130,19 @@ class Workflow(Composite): integrity of workflows when they're used somewhere else? """ - def __init__(self, label: str, *nodes: Node, strict_naming=True): - super().__init__(label=label, parent=None, strict_naming=strict_naming) + def __init__( + self, + label: str, + *nodes: Node, + run_on_updates: bool = True, + strict_naming=True + ): + super().__init__( + label=label, + parent=None, + run_on_updates=run_on_updates, + strict_naming=strict_naming, + ) for node in nodes: self.add_node(node) diff --git a/tests/unit/workflow/test_function.py b/tests/unit/workflow/test_function.py index d0251eae4..4d40382ad 100644 --- a/tests/unit/workflow/test_function.py +++ b/tests/unit/workflow/test_function.py @@ -1,8 +1,10 @@ -import unittest +from concurrent.futures import Future from sys import version_info from typing import Optional, Union +import unittest import warnings +from pyiron_contrib.executors import CloudpickleProcessPoolExecutor from pyiron_contrib.workflow.channels import NotData from pyiron_contrib.workflow.files import DirectoryObject from pyiron_contrib.workflow.function import ( @@ -279,6 +281,13 @@ def with_self(self, x: float) -> float: msg="Function functions should be able to modify attributes on the node object." ) + node.executor = CloudpickleProcessPoolExecutor + with self.assertRaises(NotImplementedError): + # Submitting node_functions that use self is still raising + # TypeError: cannot pickle '_thread.lock' object + # For now we just fail cleanly + node.run() + def with_messed_self(x: float, self) -> float: return x + 0.1 @@ -334,6 +343,75 @@ def test_call(self): # there should just be a warning that the data didn't get updated node(some_randome_kwaaaaarg="foo") + def test_return_value(self): + node = Function(plus_one) + + with self.subTest("Run on main process"): + return_on_call = node(1) + self.assertEqual( + return_on_call, + plus_one(1), + msg="Run output should be returned on call" + ) + + return_on_update = node.update() + self.assertEqual( + return_on_update, + plus_one(1), + msg="Run output should be returned on update" + ) + + node.run_on_updates = False + return_on_update_without_run = node.update() + self.assertIsNone( + return_on_update_without_run, + msg="When not running on updates, the update should not return anything" + ) + return_on_call_without_run = node(2) + self.assertIsNone( + return_on_call_without_run, + msg="When not running on updates, the call should not return anything" + ) + return_on_explicit_run = node.run() + self.assertEqual( + return_on_explicit_run, + plus_one(2), + msg="On explicit run, the most recent input data should be used and the " + "result should be returned" + ) + + with self.subTest("Run on executor"): + node.executor = CloudpickleProcessPoolExecutor() + node.run_on_updates = False + + return_on_update_without_run = node.update() + self.assertIsNone( + return_on_update_without_run, + msg="When not running on updates, the update should not return " + "anything whether there is an executor or not" + ) + return_on_explicit_run = node.run() + self.assertIsInstance( + return_on_explicit_run, + Future, + msg="Running with an executor should return the future" + ) + with self.assertRaises(RuntimeError): + # The executor run should take a second + # So we can double check that attempting to run while already running + # raises an error + node.run() + node.future.result() # Wait for the remote execution to finish + + node.run_on_updates = True + return_on_update_with_run = node.update() + self.assertIsInstance( + return_on_update_with_run, + Future, + msg="Updating should return the same as run when we get a run from the " + "update, obviously..." + ) + node.future.result() # Wait for the remote execution to finish @unittest.skipUnless(version_info[0] == 3 and version_info[1] >= 10, "Only supported for 3.10+") class TestSlow(unittest.TestCase): diff --git a/tests/unit/workflow/test_workflow.py b/tests/unit/workflow/test_workflow.py index b0bf3751c..7a8efac73 100644 --- a/tests/unit/workflow/test_workflow.py +++ b/tests/unit/workflow/test_workflow.py @@ -5,10 +5,11 @@ from pyiron_contrib.workflow.channels import NotData from pyiron_contrib.workflow.files import DirectoryObject from pyiron_contrib.workflow.function import Function +from pyiron_contrib.workflow.util import DotDict from pyiron_contrib.workflow.workflow import Workflow -def fnc(x=0): +def plus_one(x=0): y = x + 1 return y @@ -20,10 +21,10 @@ def test_node_addition(self): wf = Workflow("my_workflow") # Validate the four ways to add a node - wf.add(Function(fnc, label="foo")) - wf.add.Function(fnc, label="bar") - wf.baz = Function(fnc, label="whatever_baz_gets_used") - Function(fnc, label="qux", parent=wf) + wf.add(Function(plus_one, label="foo")) + wf.add.Function(plus_one, label="bar") + wf.baz = Function(plus_one, label="whatever_baz_gets_used") + Function(plus_one, label="qux", parent=wf) self.assertListEqual(list(wf.nodes.keys()), ["foo", "bar", "baz", "qux"]) wf.boa = wf.qux self.assertListEqual( @@ -34,13 +35,13 @@ def test_node_addition(self): wf.strict_naming = False # Validate name incrementation - wf.add(Function(fnc, label="foo")) - wf.add.Function(fnc, label="bar") + wf.add(Function(plus_one, label="foo")) + wf.add.Function(plus_one, label="bar") wf.baz = Function( - fnc, + plus_one, label="without_strict_you_can_override_by_assignment" ) - Function(fnc, label="boa", parent=wf) + Function(plus_one, label="boa", parent=wf) self.assertListEqual( list(wf.nodes.keys()), [ @@ -52,16 +53,16 @@ def test_node_addition(self): wf.strict_naming = True # Validate name preservation with self.assertRaises(AttributeError): - wf.add(Function(fnc, label="foo")) + wf.add(Function(plus_one, label="foo")) with self.assertRaises(AttributeError): - wf.add.Function(fnc, label="bar") + wf.add.Function(plus_one, label="bar") with self.assertRaises(AttributeError): - wf.baz = Function(fnc, label="whatever_baz_gets_used") + wf.baz = Function(plus_one, label="whatever_baz_gets_used") with self.assertRaises(AttributeError): - Function(fnc, label="boa", parent=wf) + Function(plus_one, label="boa", parent=wf) def test_node_packages(self): wf = Workflow("my_workflow") @@ -80,8 +81,8 @@ def test_node_packages(self): def test_double_workfloage_and_node_removal(self): wf1 = Workflow("one") - wf1.add.Function(fnc, label="node1") - node2 = Function(fnc, label="node2", parent=wf1, x=wf1.node1.outputs.y) + wf1.add.Function(plus_one, label="node1") + node2 = Function(plus_one, label="node2", parent=wf1, x=wf1.node1.outputs.y) self.assertTrue(node2.connected) wf2 = Workflow("two") @@ -95,9 +96,9 @@ def test_double_workfloage_and_node_removal(self): def test_workflow_io(self): wf = Workflow("wf") - wf.add.Function(fnc, label="n1") - wf.add.Function(fnc, label="n2") - wf.add.Function(fnc, label="n3") + wf.add.Function(plus_one, label="n1") + wf.add.Function(plus_one, label="n2") + wf.add.Function(plus_one, label="n3") with self.subTest("Workflow IO should be drawn from its nodes"): self.assertEqual(len(wf.inputs), 3) @@ -122,8 +123,10 @@ def test_working_directory(self): self.assertTrue(wf._working_directory is None) self.assertIsInstance(wf.working_directory, DirectoryObject) self.assertTrue(str(wf.working_directory.path).endswith(wf.label)) - wf.add.Function(fnc) - self.assertTrue(str(wf.fnc.working_directory.path).endswith(wf.fnc.label)) + wf.add.Function(plus_one) + self.assertTrue( + str(wf.plus_one.working_directory.path).endswith(wf.plus_one.label) + ) wf.working_directory.delete() def test_no_parents(self): @@ -143,6 +146,14 @@ def test_no_parents(self): # In both cases, we satisfy the spec that workflow's can't have parents wf2.parent = wf + def test_executor(self): + wf = Workflow("wf") + with self.assertRaises(NotImplementedError): + # Submitting callables that use self is still raising + # TypeError: cannot pickle '_thread.lock' object + # For now we just fail cleanly + wf.executor = "literally anything other than None should raise the error" + def test_parallel_execution(self): wf = Workflow("wf") @@ -192,8 +203,8 @@ def sum(a, b): def test_call(self): wf = Workflow("wf") - wf.a = wf.add.SingleValue(fnc) - wf.b = wf.add.SingleValue(fnc) + wf.a = wf.add.SingleValue(plus_one) + wf.b = wf.add.SingleValue(plus_one) @Workflow.wrap_as.single_value_node(output_labels="sum") def sum_(a, b): @@ -207,7 +218,7 @@ def sum_(a, b): ) wf(a_x=42, b_x=42) self.assertEqual( - fnc(42) + fnc(42), + plus_one(42) + plus_one(42), wf.sum.outputs.sum.value, msg="Workflow should accept input channel kwargs and update inputs " "accordingly" @@ -219,6 +230,50 @@ def sum_(a, b): # We _must_ use kwargs wf(42, 42) + def test_return_value(self): + wf = Workflow("wf") + wf.run_on_updates = True + wf.a = wf.add.SingleValue(plus_one) + wf.b = wf.add.SingleValue(plus_one, x=wf.a) + + with self.subTest("Run on main process"): + return_on_call = wf(a_x=1) + self.assertEqual( + return_on_call, + DotDict({"b_y": 1 + 2}), + msg="Run output should be returned on call. Expecting a DotDict of " + "output values" + ) + + return_on_update = wf.update() + self.assertEqual( + return_on_update.b_y, + 1 + 2, + msg="Run output should be returned on update" + ) + + wf.run_on_updates = False + return_on_update_without_run = wf.update() + self.assertIsNone( + return_on_update_without_run, + msg="When not running on updates, the update should not return anything" + ) + return_on_call_without_run = wf(a_x=2) + self.assertIsNone( + return_on_call_without_run, + msg="When not running on updates, the call should not return anything" + ) + return_on_explicit_run = wf.run() + self.assertEqual( + return_on_explicit_run["b_y"], + 2 + 2, + msg="On explicit run, the most recent input data should be used and the " + "result should be returned" + ) + + # Note: We don't need to test running on an executor, because Workflows can't + # do that yet + if __name__ == '__main__': unittest.main()