From 2aba46300e5d1d2c1d46c1f669a34173cbc964a2 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 12 Jul 2023 15:19:53 -0700 Subject: [PATCH 01/59] Refactor: extract method for batch-updating input --- pyiron_contrib/workflow/function.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/pyiron_contrib/workflow/function.py b/pyiron_contrib/workflow/function.py index 72a426b5b..ef4f907ca 100644 --- a/pyiron_contrib/workflow/function.py +++ b/pyiron_contrib/workflow/function.py @@ -340,18 +340,28 @@ def __init__( ) self._verify_that_channels_requiring_update_all_exist() - self.run_on_updates = False - # Temporarily disable running on updates to set all initial values at once + self.run_on_updates = run_on_updates + self._batch_update_input(**kwargs) + + if update_on_instantiation: + self.update() + + def _batch_update_input(self, **kwargs): + """ + Temporarily disable running on updates to set all input values at once. + + Args: + **kwargs: input label - input value (including channels for connection) + pairs. + """ + run_on_updates, self.run_on_updates = self.run_on_updates, False for k, v in kwargs.items(): if k in self.inputs.labels: self.inputs[k] = v - elif k not in self._init_keywords: - warnings.warn(f"The keyword '{k}' was received but not used.") + elif k not in self._input_args.keys(): + warnings.warn(f"The keyword '{k}' was not found among input labels.") self.run_on_updates = run_on_updates # Restore provided value - if update_on_instantiation: - self.update() - @property def _input_args(self): return inspect.signature(self.node_function).parameters From bd20e533fb53b064c7e9ceb3595bcb274e77e574 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 12 Jul 2023 15:38:20 -0700 Subject: [PATCH 02/59] Use __call__ to batch-update inputs --- pyiron_contrib/workflow/function.py | 33 ++++++++++++++++++++++++++-- tests/unit/workflow/test_function.py | 29 ++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/pyiron_contrib/workflow/function.py b/pyiron_contrib/workflow/function.py index ef4f907ca..386b36a5d 100644 --- a/pyiron_contrib/workflow/function.py +++ b/pyiron_contrib/workflow/function.py @@ -56,6 +56,11 @@ class Function(Node): call, such that output data gets pushed after the node stops running but before then `ran` signal fires. + After a node is instantiated, its input can be updated as `*args` and/or `**kwargs` + on call. + This invokes an `update()` call, which can in turn invoke `run()` if + `run_on_updates` is set to `True`. + Args: node_function (callable): The function determining the behaviour of the node. *output_labels (str): A name for each return value of the node function. @@ -507,8 +512,32 @@ def process_run_result(self, function_output): for out, value in zip(self.outputs, function_output): out.update(value) - def __call__(self) -> None: - self.run() + def _convert_input_args_and_kwargs_to_input_kwargs(self, *args, **kwargs): + reverse_keys = list(self._input_args.keys())[::-1] + if len(args) > len(reverse_keys): + raise ValueError( + f"Received {len(args)} positional arguments, but the node {self.label}" + f"only accepts {len(reverse_keys)} inputs." + ) + + positional_keywords = reverse_keys[-len(args):] + if len(set(positional_keywords).intersection(kwargs.keys())) > 0: + raise ValueError( + f"Cannot use {set(positional_keywords).intersection(kwargs.keys())} " + f"as both positional _and_ keyword arguments" + ) + + for arg in args: + key = positional_keywords.pop() + kwargs[key] = arg + + return kwargs + + def __call__(self, *args, **kwargs) -> None: + kwargs = self._convert_input_args_and_kwargs_to_input_kwargs(*args, **kwargs) + self._batch_update_input(**kwargs) + if self.run_on_updates: + self.run() def to_dict(self): return { diff --git a/tests/unit/workflow/test_function.py b/tests/unit/workflow/test_function.py index 5d2d15869..a088b35e2 100644 --- a/tests/unit/workflow/test_function.py +++ b/tests/unit/workflow/test_function.py @@ -233,6 +233,35 @@ def with_messed_self(x: float, self) -> float: self.assertEqual(len(warning_list), 1) + def test_call(self): + node = Function(no_default, "output", run_on_updates=False) + + with self.assertRaises(ValueError): + # More input args than there are input channels + node(1, 2, 3) + + with self.assertRaises(ValueError): + # Using input as an arg _and_ a kwarg + node(1, y=2, x=3) + + node(1, y=2) + self.assertEqual( + node.inputs.x.value, 1, msg="__call__ should accept args to update input" + ) + self.assertEqual( + node.inputs.y.value, 2, msg="__call__ should accept kwargs to update input" + ) + self.assertEqual( + node.outputs.output.value, NotData, msg="__call__ should not run things" + ) + node.run_on_updates = True + node(3) # Implicitly test partial update + self.assertEqual( + no_default(3, 2), + node.outputs.output.value, + msg="__call__ should invoke update s.t. run gets called if run_on_updates" + ) + @unittest.skipUnless(version_info[0] == 3 and version_info[1] >= 10, "Only supported for 3.10+") class TestSlow(unittest.TestCase): From 0887a9bd6169e33740cd01097a261e25bc39e144 Mon Sep 17 00:00:00 2001 From: samwaseda Date: Thu, 13 Jul 2023 11:28:22 -0700 Subject: [PATCH 03/59] Copy in Sam's output parser from pyiron_contrib issue #717 And use his examples as tests --- pyiron_contrib/workflow/output_parser.py | 65 +++++++++++++++++++++++ tests/unit/workflow/test_output_parser.py | 56 +++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 pyiron_contrib/workflow/output_parser.py create mode 100644 tests/unit/workflow/test_output_parser.py diff --git a/pyiron_contrib/workflow/output_parser.py b/pyiron_contrib/workflow/output_parser.py new file mode 100644 index 000000000..e5405c239 --- /dev/null +++ b/pyiron_contrib/workflow/output_parser.py @@ -0,0 +1,65 @@ +""" +Inspects code to automatically parse return values as strings +""" + +import ast +import inspect +import re + + +def _remove_spaces_until_character(string): + pattern = r'\s+(?=\s)' + modified_string = re.sub(pattern, '', string) + return modified_string + + +class ParseOutput: + def __init__(self, function): + self._func = function + self._source = None + + @property + def func(self): + return self._func + + @property + def node_return(self): + tree = ast.parse(inspect.getsource(self.func)) + for node in ast.walk(tree): + if isinstance(node, ast.Return): + return node + + @property + def source(self): + if self._source is None: + self._source = [ + line.rsplit("\n", 1)[0] for line in inspect.getsourcelines(self.func)[0] + ] + return self._source + + def get_string(self, node): + string = "" + for ll in range(node.lineno - 1, node.end_lineno): + if ll == node.lineno - 1 == node.end_lineno - 1: + string += _remove_spaces_until_character( + self.source[ll][node.col_offset:node.end_col_offset] + ) + elif ll == node.lineno - 1: + string += _remove_spaces_until_character( + self.source[ll][node.col_offset:] + ) + elif ll == node.end_lineno - 1: + string += _remove_spaces_until_character( + self.source[ll][:node.end_col_offset] + ) + else: + string += _remove_spaces_until_character(self.source[ll]) + return string + + @property + def output(self): + if self.node_return is None: + return + if isinstance(self.node_return.value, ast.Tuple): + return [self.get_string(s) for s in self.node_return.value.dims] + return [self.get_string(self.node_return.value)] \ No newline at end of file diff --git a/tests/unit/workflow/test_output_parser.py b/tests/unit/workflow/test_output_parser.py new file mode 100644 index 000000000..87c44be45 --- /dev/null +++ b/tests/unit/workflow/test_output_parser.py @@ -0,0 +1,56 @@ +from sys import version_info +import unittest + +import numpy as np + +from pyiron_contrib.workflow.output_parser import ParseOutput + + +@unittest.skipUnless( + version_info[0] == 3 and version_info[1] >= 10, "Only supported for 3.10+" +) +class TestParseOutput(unittest.TestCase): + def test_parsing(self): + with self.subTest("Single return"): + def identity(x): + return x + self.assertListEqual(ParseOutput(identity).output, ["x"]) + + with self.subTest("Expression return"): + def add(x, y): + return x + y + self.assertListEqual(ParseOutput(add).output, ["x + y"]) + + with self.subTest("Weird whitespace"): + def add(x, y): + return x + y + self.assertListEqual(ParseOutput(add).output, ["x + y"]) + + with self.subTest("Multiple expressions"): + def add_and_subtract(x, y): + return x + y, x - y + self.assertListEqual(ParseOutput(add).output, ["x + y", "x - y"]) + + with self.subTest("Best-practice (well-named return vars)"): + def md(job): + temperature = job.output.temperature + energy = job.output.energy + return temperature, energy + self.assertListEqual(ParseOutput(md).output, ["temperature", "energy"]) + + with self.subTest("Function call returns"): + def function_return(i, j): + return ( + np.arange( + i, dtype=int + ), + np.shape(i, j) + ) + self.assertListEqual( + ParseOutput(function_return).output, + ["np.arange( i, dtype=int )", "np.shape(i, j)"] + ) + + +if __name__ == '__main__': + unittest.main() From a9fd2d71a03110ee3db48efc789e5a9a9559e282 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 13 Jul 2023 12:06:16 -0700 Subject: [PATCH 04/59] :bug: fix typo calling the wrong function --- tests/unit/workflow/test_output_parser.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/unit/workflow/test_output_parser.py b/tests/unit/workflow/test_output_parser.py index 87c44be45..99d87ddd1 100644 --- a/tests/unit/workflow/test_output_parser.py +++ b/tests/unit/workflow/test_output_parser.py @@ -29,7 +29,10 @@ def add(x, y): with self.subTest("Multiple expressions"): def add_and_subtract(x, y): return x + y, x - y - self.assertListEqual(ParseOutput(add).output, ["x + y", "x - y"]) + self.assertListEqual( + ParseOutput(add_and_subtract).output, + ["x + y", "x - y"] + ) with self.subTest("Best-practice (well-named return vars)"): def md(job): From 2d653406e0e7ed8670fe49078c4e1e12e6796870 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 13 Jul 2023 12:06:34 -0700 Subject: [PATCH 05/59] Handle functions that have non-zero indentation --- pyiron_contrib/workflow/output_parser.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pyiron_contrib/workflow/output_parser.py b/pyiron_contrib/workflow/output_parser.py index e5405c239..3caa91cf2 100644 --- a/pyiron_contrib/workflow/output_parser.py +++ b/pyiron_contrib/workflow/output_parser.py @@ -5,6 +5,7 @@ import ast import inspect import re +from textwrap import dedent def _remove_spaces_until_character(string): @@ -22,9 +23,13 @@ def __init__(self, function): def func(self): return self._func + @property + def dedented_source_string(self): + return dedent(inspect.getsource(self.func)) + @property def node_return(self): - tree = ast.parse(inspect.getsource(self.func)) + tree = ast.parse(self.dedented_source_string) for node in ast.walk(tree): if isinstance(node, ast.Return): return node @@ -32,9 +37,7 @@ def node_return(self): @property def source(self): if self._source is None: - self._source = [ - line.rsplit("\n", 1)[0] for line in inspect.getsourcelines(self.func)[0] - ] + self._source = self.dedented_source_string.split("\n")[:-1] return self._source def get_string(self, node): From ea605de1cbd9940b71f80cfe710c903aa67d91c7 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 13 Jul 2023 12:11:09 -0700 Subject: [PATCH 06/59] Add a test for methods --- tests/unit/workflow/test_output_parser.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/unit/workflow/test_output_parser.py b/tests/unit/workflow/test_output_parser.py index 99d87ddd1..b10994ec6 100644 --- a/tests/unit/workflow/test_output_parser.py +++ b/tests/unit/workflow/test_output_parser.py @@ -54,6 +54,12 @@ def function_return(i, j): ["np.arange( i, dtype=int )", "np.shape(i, j)"] ) + with self.subTest("Methods too"): + class Foo: + def add(self, x, y): + return x + y + self.assertListEqual(ParseOutput(Foo.add).output, ["x + y"]) + if __name__ == '__main__': unittest.main() From b472e3fd990883c9fefdc85ca59626e8ac2c5dde Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 13 Jul 2023 12:21:21 -0700 Subject: [PATCH 07/59] Control flow of output attribute more tightly --- pyiron_contrib/workflow/output_parser.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyiron_contrib/workflow/output_parser.py b/pyiron_contrib/workflow/output_parser.py index 3caa91cf2..e4df57929 100644 --- a/pyiron_contrib/workflow/output_parser.py +++ b/pyiron_contrib/workflow/output_parser.py @@ -63,6 +63,7 @@ def get_string(self, node): def output(self): if self.node_return is None: return - if isinstance(self.node_return.value, ast.Tuple): + elif isinstance(self.node_return.value, ast.Tuple): return [self.get_string(s) for s in self.node_return.value.dims] - return [self.get_string(self.node_return.value)] \ No newline at end of file + else: + return [self.get_string(self.node_return.value)] From 6a1184136ebca2897e5a5e462d8f5c9e3d06797a Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 13 Jul 2023 12:32:28 -0700 Subject: [PATCH 08/59] Handle and test None returns --- pyiron_contrib/workflow/output_parser.py | 8 ++++++-- tests/unit/workflow/test_output_parser.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/pyiron_contrib/workflow/output_parser.py b/pyiron_contrib/workflow/output_parser.py index e4df57929..2dea2249e 100644 --- a/pyiron_contrib/workflow/output_parser.py +++ b/pyiron_contrib/workflow/output_parser.py @@ -61,9 +61,13 @@ def get_string(self, node): @property def output(self): - if self.node_return is None: + if self.node_return is None or self.node_return.value is None: return elif isinstance(self.node_return.value, ast.Tuple): return [self.get_string(s) for s in self.node_return.value.dims] else: - return [self.get_string(self.node_return.value)] + out = [self.get_string(self.node_return.value)] + if out == ["None"]: + return + else: + return out diff --git a/tests/unit/workflow/test_output_parser.py b/tests/unit/workflow/test_output_parser.py index b10994ec6..f83c96012 100644 --- a/tests/unit/workflow/test_output_parser.py +++ b/tests/unit/workflow/test_output_parser.py @@ -60,6 +60,22 @@ def add(self, x, y): return x + y self.assertListEqual(ParseOutput(Foo.add).output, ["x + y"]) + def test_void(self): + with self.subTest("No return"): + def no_return(): + pass + self.assertIsNone(ParseOutput(no_return).output) + + with self.subTest("Empty return"): + def empty_return(): + return + self.assertIsNone(ParseOutput(empty_return).output) + + with self.subTest("Return None explicitly"): + def none_return(): + return None + self.assertIsNone(ParseOutput(none_return).output) + if __name__ == '__main__': unittest.main() From 31abcfb97bbb9ba0ef89581c4e7b30896e3d585e Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 13 Jul 2023 12:59:11 -0700 Subject: [PATCH 09/59] Raise an exception when multiple return values are encountered --- pyiron_contrib/workflow/output_parser.py | 14 +++++++++++++- tests/unit/workflow/test_output_parser.py | 9 +++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/pyiron_contrib/workflow/output_parser.py b/pyiron_contrib/workflow/output_parser.py index 2dea2249e..1a166dbd1 100644 --- a/pyiron_contrib/workflow/output_parser.py +++ b/pyiron_contrib/workflow/output_parser.py @@ -30,9 +30,21 @@ def dedented_source_string(self): @property def node_return(self): tree = ast.parse(self.dedented_source_string) + returns = [] for node in ast.walk(tree): if isinstance(node, ast.Return): - return node + returns.append(node) + + if len(returns) > 1: + raise ValueError( + f"{self.__class__.__name__} can only parse callables with at most one " + f"return value, but ast.walk found {len(returns)}." + ) + + try: + return returns[0] + except IndexError: + return None @property def source(self): diff --git a/tests/unit/workflow/test_output_parser.py b/tests/unit/workflow/test_output_parser.py index f83c96012..ad1808cb4 100644 --- a/tests/unit/workflow/test_output_parser.py +++ b/tests/unit/workflow/test_output_parser.py @@ -76,6 +76,15 @@ def none_return(): return None self.assertIsNone(ParseOutput(none_return).output) + def test_multiple_branches(self): + def bifurcating(x): + if x > 5: + return True + else: + return False + with self.assertRaises(ValueError): + ParseOutput(bifurcating).output + if __name__ == '__main__': unittest.main() From 1ed3a6e2bde2f8b5c6280af9d46aa8a65537e6da Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 13 Jul 2023 13:00:35 -0700 Subject: [PATCH 10/59] Parse on instantiation --- pyiron_contrib/workflow/output_parser.py | 4 ++++ tests/unit/workflow/test_output_parser.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pyiron_contrib/workflow/output_parser.py b/pyiron_contrib/workflow/output_parser.py index 1a166dbd1..095471898 100644 --- a/pyiron_contrib/workflow/output_parser.py +++ b/pyiron_contrib/workflow/output_parser.py @@ -18,6 +18,7 @@ class ParseOutput: def __init__(self, function): self._func = function self._source = None + self._output = self.get_parsed_output() @property def func(self): @@ -73,6 +74,9 @@ def get_string(self, node): @property def output(self): + return self._output + + def get_parsed_output(self): if self.node_return is None or self.node_return.value is None: return elif isinstance(self.node_return.value, ast.Tuple): diff --git a/tests/unit/workflow/test_output_parser.py b/tests/unit/workflow/test_output_parser.py index ad1808cb4..e8b5f066f 100644 --- a/tests/unit/workflow/test_output_parser.py +++ b/tests/unit/workflow/test_output_parser.py @@ -83,7 +83,7 @@ def bifurcating(x): else: return False with self.assertRaises(ValueError): - ParseOutput(bifurcating).output + ParseOutput(bifurcating) if __name__ == '__main__': From 1745eef7807dd2d9c0cf2b3684538346105b1382 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 13 Jul 2023 13:04:34 -0700 Subject: [PATCH 11/59] Add docstring --- pyiron_contrib/workflow/output_parser.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pyiron_contrib/workflow/output_parser.py b/pyiron_contrib/workflow/output_parser.py index 095471898..36342cc92 100644 --- a/pyiron_contrib/workflow/output_parser.py +++ b/pyiron_contrib/workflow/output_parser.py @@ -15,6 +15,15 @@ def _remove_spaces_until_character(string): class ParseOutput: + """ + Given a function with at most one `return` expression, inspects the source code and + parses a list of strings containing the returned values. + If the function returns `None`, the parsed value is also `None`. + This parsed value is evaluated at instantiation and stored in the `output` + attribute. + In case more than one `return` expression is found, a `ValueError` is raised. + """ + def __init__(self, function): self._func = function self._source = None From d369a8614314ea8498154d227f25c3c29ca9827d Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 13 Jul 2023 14:31:16 -0700 Subject: [PATCH 12/59] _Allow_ not passing output labels --- pyiron_contrib/workflow/function.py | 3 ++- tests/unit/workflow/test_function.py | 5 +---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/pyiron_contrib/workflow/function.py b/pyiron_contrib/workflow/function.py index 72a426b5b..c8997b6a6 100644 --- a/pyiron_contrib/workflow/function.py +++ b/pyiron_contrib/workflow/function.py @@ -9,6 +9,7 @@ from pyiron_contrib.workflow.has_channel import HasChannel from pyiron_contrib.workflow.io import Inputs, Outputs, Signals from pyiron_contrib.workflow.node import Node +from pyiron_contrib.workflow.output_parser import ParseOutput if TYPE_CHECKING: from pyiron_contrib.workflow.composite import Composite @@ -322,7 +323,7 @@ def __init__( # **kwargs, ) if len(output_labels) == 0: - raise ValueError("Nodes must have at least one output label.") + output_labels = ParseOutput(node_function).output self.node_function = node_function diff --git a/tests/unit/workflow/test_function.py b/tests/unit/workflow/test_function.py index 5d2d15869..4c7ddc502 100644 --- a/tests/unit/workflow/test_function.py +++ b/tests/unit/workflow/test_function.py @@ -46,10 +46,7 @@ def test_defaults(self): ) def test_failure_without_output_labels(self): - with self.assertRaises( - ValueError, - msg="Instantiated nodes should demand at least one output label" - ): + with self.subTest("Automatically scrape output labels"): Function(plus_one) def test_instantiation_update(self): From bc203e50d0c5f2f1de5b5727f71706ab84a0ab84 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 13 Jul 2023 14:45:41 -0700 Subject: [PATCH 13/59] Modify docstring to change spec --- pyiron_contrib/workflow/function.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pyiron_contrib/workflow/function.py b/pyiron_contrib/workflow/function.py index c8997b6a6..d72b02836 100644 --- a/pyiron_contrib/workflow/function.py +++ b/pyiron_contrib/workflow/function.py @@ -59,7 +59,6 @@ class Function(Node): Args: node_function (callable): The function determining the behaviour of the node. - *output_labels (str): A name for each return value of the node function. label (str): The node's label. (Defaults to the node function's name.) run_on_updates (bool): Whether to run when you are updated and all your input is ready. (Default is True). @@ -71,6 +70,15 @@ class Function(Node): called. This can be used to create sets of input data _all_ of which must be updated before the node is ready to produce output again. (Default is None, which makes the list empty.) + output_labels (Optional[str | list[str] | tuple[str]]): A name for each return + value of the node function OR a single label. (Default is None, which + scrapes output labels automatically from the source code of the wrapped + function.) This can be useful when returned values are not well named, e.g. + to make the output channel dot-accessible if it would otherwise have a label + that requires item-string-based access. Additionally, specifying a _single_ + label for a wrapped function that returns a tuple of values ensures that a + _single_ output channel (holding the tuple) is created, instead of one + channel for each return value. **kwargs: Any additional keyword arguments whose keyword matches the label of an input channel will have their value assigned to that channel. From 716c1df6adffe750e15a4dc12bc1dc453fd70617 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 13 Jul 2023 15:13:19 -0700 Subject: [PATCH 14/59] Make output labels optional Breaks other tests, demos, and docs still --- pyiron_contrib/workflow/function.py | 29 +++++++++++-- tests/unit/workflow/test_function.py | 63 ++++++++++++++++++---------- 2 files changed, 66 insertions(+), 26 deletions(-) diff --git a/pyiron_contrib/workflow/function.py b/pyiron_contrib/workflow/function.py index d72b02836..8d24f4722 100644 --- a/pyiron_contrib/workflow/function.py +++ b/pyiron_contrib/workflow/function.py @@ -317,12 +317,12 @@ class Function(Node): def __init__( self, node_function: callable, - *output_labels: str, label: Optional[str] = None, run_on_updates: bool = True, update_on_instantiation: bool = True, channels_requiring_update_after_run: Optional[list[str]] = None, parent: Optional[Composite] = None, + output_labels: Optional[str | list[str] | tuple[str]] = None, **kwargs, ): super().__init__( @@ -330,14 +330,12 @@ def __init__( parent=parent, # **kwargs, ) - if len(output_labels) == 0: - output_labels = ParseOutput(node_function).output self.node_function = node_function self._inputs = None self._outputs = None - self._output_labels = output_labels + self._output_labels = self._get_output_labels(output_labels) # TODO: Parse output labels from the node function in case output_labels is None self.signals = self._build_signal_channels() @@ -361,6 +359,29 @@ def __init__( if update_on_instantiation: self.update() + def _get_output_labels(self, output_labels: str | list[str] | tuple[str] | None): + """ + Explicitly passed output labels can be used to rename awkward parsed labels, or + to force the creation of a _single_ output channel when wrapped functions return + a tuple of values. + """ + parsed_labels = ParseOutput(self.node_function).output + if output_labels is None: + return parsed_labels + else: + if isinstance(output_labels, str): + output_labels = (output_labels,) + + if len(output_labels) != 1 and len(output_labels) != len(parsed_labels): + raise ValueError( + f"When output labels are explicitly provided they must either be a " + f"_single_ label, or match the length of the parsed labels. In " + f"this case, {output_labels} were received while {parsed_labels} " + f"were parsed." + ) + + return output_labels + @property def _input_args(self): return inspect.signature(self.node_function).parameters diff --git a/tests/unit/workflow/test_function.py b/tests/unit/workflow/test_function.py index 4c7ddc502..8d684cf48 100644 --- a/tests/unit/workflow/test_function.py +++ b/tests/unit/workflow/test_function.py @@ -15,24 +15,29 @@ def throw_error(x: Optional[int] = None): def plus_one(x=1) -> Union[int, float]: - return x + 1 + y = x + 1 + return y def no_default(x, y): return x + y + 1 +def returns_multiple(x, y): + return x, y, x + y + + @unittest.skipUnless(version_info[0] == 3 and version_info[1] >= 10, "Only supported for 3.10+") class TestFunction(unittest.TestCase): def test_defaults(self): - with_defaults = Function(plus_one, "y") + with_defaults = Function(plus_one) self.assertEqual( with_defaults.inputs.x.value, 1, msg=f"Expected to get the default provided in the underlying function but " f"got {with_defaults.inputs.x.value}", ) - without_defaults = Function(no_default, "sum_plus_one") + without_defaults = Function(no_default) self.assertIs( without_defaults.inputs.x.value, NotData, @@ -45,14 +50,26 @@ def test_defaults(self): "defaults, the node should not be ready!" ) - def test_failure_without_output_labels(self): + def test_label_choices(self): with self.subTest("Automatically scrape output labels"): - Function(plus_one) + n = Function(plus_one) + self.assertListEqual(n.outputs.labels, ["y"]) + + with self.subTest("Allow overriding them"): + n = Function(no_default, output_labels=("sum_plus_one",)) + self.assertListEqual(n.outputs.labels, ["sum_plus_one"]) + + with self.subTest("Allow forcing _one_ output channel"): + n = Function(returns_multiple, output_labels="its_a_tuple") + self.assertListEqual(n.outputs.labels, ["its_a_tuple"]) + + with self.subTest("Force matching lengths"): + with self.assertRaises(ValueError): + Function(returns_multiple, output_labels=["one", "two"]) def test_instantiation_update(self): no_update = Function( plus_one, - "y", run_on_updates=True, update_on_instantiation=False ) @@ -65,13 +82,12 @@ def test_instantiation_update(self): update = Function( plus_one, - "y", run_on_updates=True, update_on_instantiation=True ) self.assertEqual(2, update.outputs.y.value) - default = Function(plus_one, "y") + default = Function(plus_one) self.assertEqual( 2, default.outputs.y.value, @@ -80,19 +96,19 @@ def test_instantiation_update(self): ) with self.assertRaises(TypeError): - run_without_value = Function(no_default, "z") + run_without_value = Function(no_default) run_without_value.run() # None + None + 1 -> error with self.assertRaises(TypeError): - run_without_value = Function(no_default, "z", x=1) + run_without_value = Function(no_default, x=1) run_without_value.run() # 1 + None + 1 -> error - deferred_update = Function(no_default, "z", x=1, y=1) + deferred_update = Function(no_default, x=1, y=1) deferred_update.run() self.assertEqual( - deferred_update.outputs.z.value, + deferred_update.outputs["x + y + 1"].value, 3, msg="By default, all initial values should be parsed before triggering " "an update" @@ -117,35 +133,38 @@ def test_automatic_updates(self): node.inputs.x.update(1) def test_signals(self): - @function_node("y") + @function_node() def linear(x): return x - @function_node("z") + @function_node() def times_two(y): return 2 * y l = linear(x=1) t2 = times_two( - update_on_instantiation=False, run_automatically=False, y=l.outputs.y + update_on_instantiation=False, + run_automatically=False, + output_labels=["double"], + y=l.outputs.x ) self.assertIs( - t2.outputs.z.value, + t2.outputs.double.value, NotData, msg=f"Without updates, expected the output to be {NotData} but got " - f"{t2.outputs.z.value}" + f"{t2.outputs.double.value}" ) # Nodes should _all_ have the run and ran signals t2.signals.input.run = l.signals.output.ran l.run() self.assertEqual( - t2.outputs.z.value, 2, + t2.outputs.double.value, 2, msg="Running the upstream node should trigger a run here" ) def test_statuses(self): - n = Function(plus_one, "p1", run_on_updates=False) + n = Function(plus_one, run_on_updates=False) self.assertTrue(n.ready) self.assertFalse(n.running) self.assertFalse(n.failed) @@ -197,7 +216,7 @@ def with_self(self, x: float) -> float: self.some_counter = 1 return x + 0.1 - node = Function(with_self, "output") + node = Function(with_self, output_labels="output") self.assertTrue( "x" in node.inputs.labels, msg=f"Expected to find function input 'x' in the node input but got " @@ -225,7 +244,7 @@ def with_messed_self(x: float, self) -> float: return x + 0.1 with warnings.catch_warnings(record=True) as warning_list: - node = Function(with_messed_self, "output") + node = Function(with_messed_self) self.assertTrue("self" in node.inputs.labels) self.assertEqual(len(warning_list), 1) @@ -234,7 +253,7 @@ def with_messed_self(x: float, self) -> float: @unittest.skipUnless(version_info[0] == 3 and version_info[1] >= 10, "Only supported for 3.10+") class TestSlow(unittest.TestCase): def test_instantiation(self): - slow = Slow(plus_one, "y") + slow = Slow(plus_one) self.assertIs( slow.outputs.y.value, NotData, From 40cf7e5394fbbaad8d3927da44d27c18bde9aa50 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 13 Jul 2023 15:16:20 -0700 Subject: [PATCH 15/59] Fix the rest of the function tests --- tests/unit/workflow/test_function.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/unit/workflow/test_function.py b/tests/unit/workflow/test_function.py index 8d684cf48..96a9b1b20 100644 --- a/tests/unit/workflow/test_function.py +++ b/tests/unit/workflow/test_function.py @@ -277,10 +277,10 @@ def test_instantiation(self): @unittest.skipUnless(version_info[0] == 3 and version_info[1] >= 10, "Only supported for 3.10+") class TestSingleValue(unittest.TestCase): def test_instantiation(self): - has_defaults_and_one_return = SingleValue(plus_one, "y") + has_defaults_and_one_return = SingleValue(plus_one) with self.assertRaises(ValueError): - too_many_labels = SingleValue(plus_one, "z", "excess_label") + too_many_labels = SingleValue(plus_one, output_labels=["z", "excess_label"]) def test_item_and_attribute_access(self): class Foo: @@ -296,7 +296,7 @@ def __getitem__(self, item): def returns_foo() -> Foo: return Foo() - svn = SingleValue(returns_foo, "foo") + svn = SingleValue(returns_foo, output_labels="foo") self.assertEqual( svn.some_attribute, @@ -326,14 +326,14 @@ def returns_foo() -> Foo: ) def test_repr(self): - svn = SingleValue(plus_one, "y") + svn = SingleValue(plus_one) self.assertEqual( svn.__repr__(), svn.outputs.y.value.__repr__(), msg="SingleValueNodes should have their output as their representation" ) def test_str(self): - svn = SingleValue(plus_one, "y") + svn = SingleValue(plus_one) self.assertTrue( str(svn).endswith(str(svn.single_value)), msg="SingleValueNodes should have their output as a string in their string " @@ -342,8 +342,8 @@ def test_str(self): ) def test_easy_output_connection(self): - svn = SingleValue(plus_one, "y") - regular = Function(plus_one, "y") + svn = SingleValue(plus_one) + regular = Function(plus_one) regular.inputs.x = svn @@ -360,7 +360,7 @@ def test_easy_output_connection(self): "case default->plus_one->plus_one = 1 + 1 +1 = 3" ) - at_instantiation = Function(plus_one, "y", x=svn) + at_instantiation = Function(plus_one, x=svn) self.assertIn( svn.outputs.y, at_instantiation.inputs.x.connections, msg="The parsing of SingleValue output as a connection should also work" @@ -368,7 +368,7 @@ def test_easy_output_connection(self): ) def test_channels_requiring_update_after_run(self): - @single_value_node("sum") + @single_value_node(output_labels="sum") def my_node(x: int = 0, y: int = 0, z: int = 0): return x + y + z @@ -420,7 +420,7 @@ def my_node(x: int = 0, y: int = 0, z: int = 0): ) def test_working_directory(self): - n_f = Function(plus_one, "output") + n_f = Function(plus_one) self.assertTrue(n_f._working_directory is None) self.assertIsInstance(n_f.working_directory, DirectoryObject) self.assertTrue(str(n_f.working_directory.path).endswith(n_f.label)) From 6c9c4894c0223ef7d73e0f4442023b98a54a816e Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 13 Jul 2023 15:34:24 -0700 Subject: [PATCH 16/59] Propagate changes to the rest of code and tests No docs or demo yet --- pyiron_contrib/workflow/function.py | 28 ++++---- .../workflow/node_library/atomistics.py | 64 ++++++++++--------- .../workflow/node_library/standard.py | 2 +- tests/unit/workflow/test_node_package.py | 9 +-- tests/unit/workflow/test_workflow.py | 47 +++++++------- 5 files changed, 76 insertions(+), 74 deletions(-) diff --git a/pyiron_contrib/workflow/function.py b/pyiron_contrib/workflow/function.py index 8d24f4722..1dda66513 100644 --- a/pyiron_contrib/workflow/function.py +++ b/pyiron_contrib/workflow/function.py @@ -553,20 +553,20 @@ class Slow(Function): def __init__( self, node_function: callable, - *output_labels: str, label: Optional[str] = None, run_on_updates=False, update_on_instantiation=False, parent: Optional[Workflow] = None, + output_labels: Optional[str | list[str] | tuple[str]] = None, **kwargs, ): super().__init__( node_function, - *output_labels, label=label, run_on_updates=run_on_updates, update_on_instantiation=update_on_instantiation, parent=parent, + output_labels=output_labels, **kwargs, ) @@ -581,31 +581,31 @@ class SingleValue(Function, HasChannel): def __init__( self, node_function: callable, - *output_labels: str, label: Optional[str] = None, run_on_updates=True, update_on_instantiation=True, parent: Optional[Workflow] = None, + output_labels: Optional[str | list[str] | tuple[str]] = None, **kwargs, ): - self.ensure_there_is_only_one_return_value(output_labels) super().__init__( node_function, - *output_labels, label=label, run_on_updates=run_on_updates, update_on_instantiation=update_on_instantiation, parent=parent, + output_labels=output_labels, **kwargs, ) - @classmethod - def ensure_there_is_only_one_return_value(cls, output_labels): + def _get_output_labels(self, output_labels: str | list[str] | tuple[str] | None): + output_labels = super()._get_output_labels(output_labels) if len(output_labels) > 1: raise ValueError( - f"{cls.__name__} must only have a single return value, but got " - f"multiple output labels: {output_labels}" + f"{self.__class__.__name__} must only have a single return value, but " + f"got multiple output labels: {output_labels}" ) + return output_labels @property def single_value(self): @@ -631,7 +631,7 @@ def __str__(self): ) -def function_node(*output_labels: str, **node_class_kwargs): +def function_node(**node_class_kwargs): """ A decorator for dynamically creating node classes from functions. @@ -650,7 +650,6 @@ def as_node(node_function: callable): "__init__": partialmethod( Function.__init__, node_function, - *output_labels, **node_class_kwargs, ) }, @@ -659,7 +658,7 @@ def as_node(node_function: callable): return as_node -def slow_node(*output_labels: str, **node_class_kwargs): +def slow_node(**node_class_kwargs): """ A decorator for dynamically creating slow node classes from functions. @@ -676,7 +675,6 @@ def as_slow_node(node_function: callable): "__init__": partialmethod( Slow.__init__, node_function, - *output_labels, **node_class_kwargs, ) }, @@ -685,7 +683,7 @@ def as_slow_node(node_function: callable): return as_slow_node -def single_value_node(*output_labels: str, **node_class_kwargs): +def single_value_node(**node_class_kwargs): """ A decorator for dynamically creating fast node classes from functions. @@ -693,7 +691,6 @@ def single_value_node(*output_labels: str, **node_class_kwargs): """ def as_single_value_node(node_function: callable): - SingleValue.ensure_there_is_only_one_return_value(output_labels) return type( node_function.__name__.title().replace("_", ""), # fnc_name to CamelCase (SingleValue,), # Define parentage @@ -701,7 +698,6 @@ def as_single_value_node(node_function: callable): "__init__": partialmethod( SingleValue.__init__, node_function, - *output_labels, **node_class_kwargs, ) }, diff --git a/pyiron_contrib/workflow/node_library/atomistics.py b/pyiron_contrib/workflow/node_library/atomistics.py index 1da3bdac6..fd304570e 100644 --- a/pyiron_contrib/workflow/node_library/atomistics.py +++ b/pyiron_contrib/workflow/node_library/atomistics.py @@ -10,12 +10,12 @@ from pyiron_contrib.workflow.function import single_value_node, slow_node -@single_value_node("structure") +@single_value_node(output_labels="structure") def bulk_structure(element: str = "Fe", cubic: bool = False, repeat: int = 1) -> Atoms: return _StructureFactory().bulk(element, cubic=cubic).repeat(repeat) -@single_value_node("job") +@single_value_node(output_labels="job") def lammps(structure: Optional[Atoms] = None) -> LammpsJob: pr = Project(".") job = pr.atomistics.job.Lammps("NOTAREALNAME") @@ -82,20 +82,22 @@ def _run_and_remove_job(job, modifier: Optional[callable] = None, **modifier_kwa @slow_node( - "cells", - "displacements", - "energy_pot", - "energy_tot", - "force_max", - "forces", - "indices", - "positions", - "pressures", - "steps", - "temperature", - "total_displacements", - "unwrapped_positions", - "volume", + output_labels=[ + "cells", + "displacements", + "energy_pot", + "energy_tot", + "force_max", + "forces", + "indices", + "positions", + "pressures", + "steps", + "temperature", + "total_displacements", + "unwrapped_positions", + "volume", + ] ) def calc_static( job: AtomisticGenericJob, @@ -104,20 +106,22 @@ def calc_static( @slow_node( - "cells", - "displacements", - "energy_pot", - "energy_tot", - "force_max", - "forces", - "indices", - "positions", - "pressures", - "steps", - "temperature", - "total_displacements", - "unwrapped_positions", - "volume", + output_labels= [ + "cells", + "displacements", + "energy_pot", + "energy_tot", + "force_max", + "forces", + "indices", + "positions", + "pressures", + "steps", + "temperature", + "total_displacements", + "unwrapped_positions", + "volume", + ] ) def calc_md( job: AtomisticGenericJob, diff --git a/pyiron_contrib/workflow/node_library/standard.py b/pyiron_contrib/workflow/node_library/standard.py index a920e2538..1a2d11e21 100644 --- a/pyiron_contrib/workflow/node_library/standard.py +++ b/pyiron_contrib/workflow/node_library/standard.py @@ -8,7 +8,7 @@ from pyiron_contrib.workflow.function import single_value_node -@single_value_node("fig") +@single_value_node(output_labels="fig") def scatter( x: Optional[list | np.ndarray] = None, y: Optional[list | np.ndarray] = None ): diff --git a/tests/unit/workflow/test_node_package.py b/tests/unit/workflow/test_node_package.py index c8492437c..4e89db0b4 100644 --- a/tests/unit/workflow/test_node_package.py +++ b/tests/unit/workflow/test_node_package.py @@ -5,7 +5,7 @@ from pyiron_contrib.workflow.workflow import Workflow -@Workflow.wrap_as.function_node("x") +@Workflow.wrap_as.function_node() def dummy(x: int = 0): return x @@ -41,7 +41,7 @@ def test_update(self): with self.assertRaises(TypeError): self.package.available_name = "But we can still only assign node classes" - @Workflow.wrap_as.function_node("y") + @Workflow.wrap_as.function_node(output_label="y") def add(x: int = 0): return x + 1 @@ -53,9 +53,10 @@ def add(x: int = 0): old_dummy_instance = self.package.Dummy(label="old_dummy_instance") - @Workflow.wrap_as.function_node("y") + @Workflow.wrap_as.function_node() def dummy(x: int = 0): - return x + 1 + y = x + 1 + return y self.package.update(dummy) diff --git a/tests/unit/workflow/test_workflow.py b/tests/unit/workflow/test_workflow.py index db35843ed..b9dd2fd4a 100644 --- a/tests/unit/workflow/test_workflow.py +++ b/tests/unit/workflow/test_workflow.py @@ -9,7 +9,8 @@ def fnc(x=0): - return x + 1 + y = x + 1 + return y @unittest.skipUnless(version_info[0] == 3 and version_info[1] >= 10, "Only supported for 3.10+") @@ -19,10 +20,10 @@ def test_node_addition(self): wf = Workflow("my_workflow") # Validate the four ways to add a node - wf.add(Function(fnc, "x", label="foo")) - wf.add.Function(fnc, "y", label="bar") - wf.baz = Function(fnc, "y", label="whatever_baz_gets_used") - Function(fnc, "x", label="qux", parent=wf) + 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) self.assertListEqual(list(wf.nodes.keys()), ["foo", "bar", "baz", "qux"]) wf.boa = wf.qux self.assertListEqual( @@ -33,14 +34,13 @@ def test_node_addition(self): wf.strict_naming = False # Validate name incrementation - wf.add(Function(fnc, "x", label="foo")) - wf.add.Function(fnc, "y", label="bar") + wf.add(Function(fnc, label="foo")) + wf.add.Function(fnc, label="bar") wf.baz = Function( fnc, - "y", label="without_strict_you_can_override_by_assignment" ) - Function(fnc, "x", label="boa", parent=wf) + Function(fnc, label="boa", parent=wf) self.assertListEqual( list(wf.nodes.keys()), [ @@ -52,16 +52,16 @@ def test_node_addition(self): wf.strict_naming = True # Validate name preservation with self.assertRaises(AttributeError): - wf.add(Function(fnc, "x", label="foo")) + wf.add(Function(fnc, label="foo")) with self.assertRaises(AttributeError): - wf.add.Function(fnc, "y", label="bar") + wf.add.Function(fnc, label="bar") with self.assertRaises(AttributeError): - wf.baz = Function(fnc, "y", label="whatever_baz_gets_used") + wf.baz = Function(fnc, label="whatever_baz_gets_used") with self.assertRaises(AttributeError): - Function(fnc, "x", label="boa", parent=wf) + Function(fnc, label="boa", parent=wf) def test_node_packages(self): wf = Workflow("my_workflow") @@ -80,8 +80,8 @@ def test_node_packages(self): def test_double_workfloage_and_node_removal(self): wf1 = Workflow("one") - wf1.add.Function(fnc, "y", label="node1") - node2 = Function(fnc, "y", label="node2", parent=wf1, x=wf1.node1.outputs.y) + wf1.add.Function(fnc, label="node1") + node2 = Function(fnc, label="node2", parent=wf1, x=wf1.node1.outputs.y) self.assertTrue(node2.connected) wf2 = Workflow("two") @@ -95,9 +95,9 @@ def test_double_workfloage_and_node_removal(self): def test_workflow_io(self): wf = Workflow("wf") - wf.add.Function(fnc, "y", label="n1") - wf.add.Function(fnc, "y", label="n2") - wf.add.Function(fnc, "y", label="n3") + wf.add.Function(fnc, label="n1") + wf.add.Function(fnc, label="n2") + wf.add.Function(fnc, label="n3") with self.subTest("Workflow IO should be drawn from its nodes"): self.assertEqual(len(wf.inputs), 3) @@ -111,7 +111,7 @@ def test_workflow_io(self): self.assertEqual(len(wf.outputs), 1) def test_node_decorator_access(self): - @Workflow.wrap_as.function_node("y") + @Workflow.wrap_as.function_node(output_labels="y") def plus_one(x: int = 0) -> int: return x + 1 @@ -122,7 +122,7 @@ 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, "output") + wf.add.Function(fnc) self.assertTrue(str(wf.fnc.working_directory.path).endswith(wf.fnc.label)) wf.working_directory.delete() @@ -146,12 +146,13 @@ def test_no_parents(self): def test_parallel_execution(self): wf = Workflow("wf") - @Workflow.wrap_as.single_value_node("five", run_on_updates=False) + @Workflow.wrap_as.single_value_node(run_on_updates=False) def five(sleep_time=0.): sleep(sleep_time) - return 5 + five = 5 + return five - @Workflow.wrap_as.single_value_node("sum") + @Workflow.wrap_as.single_value_node(output_labels="sum") def sum(a, b): return a + b From b1d4680ce921651a0980adde7a3a5445437c133f Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 14 Jul 2023 13:21:30 -0700 Subject: [PATCH 17/59] Handle no data output --- pyiron_contrib/workflow/function.py | 6 ++++-- tests/unit/workflow/test_function.py | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pyiron_contrib/workflow/function.py b/pyiron_contrib/workflow/function.py index 1dda66513..e799e715f 100644 --- a/pyiron_contrib/workflow/function.py +++ b/pyiron_contrib/workflow/function.py @@ -367,7 +367,7 @@ def _get_output_labels(self, output_labels: str | list[str] | tuple[str] | None) """ parsed_labels = ParseOutput(self.node_function).output if output_labels is None: - return parsed_labels + return parsed_labels if parsed_labels is not None else [] else: if isinstance(output_labels, str): output_labels = (output_labels,) @@ -521,7 +521,9 @@ def process_run_result(self, function_output): for channel_name in self.channels_requiring_update_after_run: self.inputs[channel_name].wait_for_update() - if len(self.outputs) == 1: + if len(self.outputs) == 0: + return + elif len(self.outputs) == 1: function_output = (function_output,) for out, value in zip(self.outputs, function_output): diff --git a/tests/unit/workflow/test_function.py b/tests/unit/workflow/test_function.py index 96a9b1b20..dac78dd1d 100644 --- a/tests/unit/workflow/test_function.py +++ b/tests/unit/workflow/test_function.py @@ -26,9 +26,17 @@ def no_default(x, y): def returns_multiple(x, y): return x, y, x + y +def void(): + pass + @unittest.skipUnless(version_info[0] == 3 and version_info[1] >= 10, "Only supported for 3.10+") class TestFunction(unittest.TestCase): + def test_instantiation(self): + with self.subTest("Void function"): + void_node = Function(void) + self.assertEqual(len(void_node.outputs), 0) + def test_defaults(self): with_defaults = Function(plus_one) self.assertEqual( From a2832f4451f961365f2fed9b617b1243938f6572 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 14 Jul 2023 13:46:20 -0700 Subject: [PATCH 18/59] Update example --- notebooks/workflow_example.ipynb | 220 ++++++++++++++++++++----------- 1 file changed, 140 insertions(+), 80 deletions(-) diff --git a/notebooks/workflow_example.ipynb b/notebooks/workflow_example.ipynb index 50843a9e8..1ef82d6f8 100644 --- a/notebooks/workflow_example.ipynb +++ b/notebooks/workflow_example.ipynb @@ -9,7 +9,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "d57449473dbc42f2997863543b5171c6", + "model_id": "e23eaad8312941fbbaa0683e71bc8ed6", "version_major": 2, "version_minor": 0 }, @@ -47,7 +47,9 @@ "source": [ "## Instantiating a node\n", "\n", - "Simple nodes can be defined on-the-fly by passing any callable to the `Function(Node)` class, along with a string (tuple of strings) giving names for the output value(s)." + "Simple nodes can be defined on-the-fly by passing any callable to the `Function(Node)` class. This transforms the function into a node instance which has input and output, can be connected to other nodes in a workflow, and can run the function it stores.\n", + "\n", + "Input and output channels are _automatically_ extracted from the signature and return value(s) of the function. (Note: \"Nodized\" functions must have _at most_ one `return` expression!)" ] }, { @@ -60,7 +62,7 @@ "def plus_minus_one(x):\n", " return x+1, x-1\n", "\n", - "pm_node = Function(plus_minus_one, \"p1\", \"m1\")" + "pm_node = Function(plus_minus_one)" ] }, { @@ -81,7 +83,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "['x'] ['p1', 'm1']\n" + "['x'] ['x+1', 'x-1']\n" ] } ], @@ -94,8 +96,7 @@ "id": "22ee2a49-47d1-4cec-bb25-8441ea01faf7", "metadata": {}, "source": [ - "The output is still empty (`NotData`) because we haven't `run()` the node.\n", - "If we try that now though, we'll just get a type error because the input is not set! " + "The output is still empty (`NotData`) because we haven't `run()` the node:" ] }, { @@ -108,12 +109,30 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'p1': , 'm1': }\n" + "{'x+1': , 'x-1': }\n" ] } ], "source": [ - "print(pm_node.outputs.to_value_dict())\n" + "print(pm_node.outputs.to_value_dict())" + ] + }, + { + "cell_type": "markdown", + "id": "0374e277-55ab-45d2-8058-b06365bd07af", + "metadata": {}, + "source": [ + "If we try that now though, we'll just get a type error because the input is not set! " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "05196cd8-97c7-4f08-ae3a-ad6a076512f7", + "metadata": {}, + "outputs": [], + "source": [ + "# pm_node.run()" ] }, { @@ -124,12 +143,12 @@ "By default, a softer `update()` call is made at instantiation and whenever the node input is updated.\n", "This call checks to make sure the input is `ready` before moving on to `run()`. \n", "\n", - "If we update the input, we'll give the node enough data to work with and it will automatically update the output" + "If we update the input, we'll give the node enough data to work with and it will automatically update the output:" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "id": "b1500a40-f4f2-4c06-ad78-aaebcf3e9a50", "metadata": {}, "outputs": [ @@ -137,7 +156,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'p1': 6, 'm1': 4}\n" + "{'x+1': 6, 'x-1': 4}\n" ] } ], @@ -151,12 +170,14 @@ "id": "df4520d7-856e-4bc8-817f-5b2e22c1ddce", "metadata": {}, "source": [ - "We can be stricter and force the node to wait for an explicit `run()` call by modifying the `run_on_updates` and `update_on_instantiation` flags." + "We can be stricter and force the node to wait for an explicit `run()` call by modifying the `run_on_updates` and `update_on_instantiation` flags. \n", + "\n", + "Let's also take the opportunity to give our output channel a better name so we can get it by dot-access." ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "id": "ab1ac28a-6e69-491f-882f-da4a43162dd7", "metadata": {}, "outputs": [ @@ -166,18 +187,19 @@ "pyiron_contrib.workflow.channels.NotData" ] }, - "execution_count": 6, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def adder(x: int, y: int = 1) -> int:\n", - " return x + y\n", + " sum_ = x + y\n", + " return sum_\n", "\n", - "adder_node = Function(adder, \"sum\", run_on_updates=False)\n", + "adder_node = Function(adder, run_on_updates=False)\n", "adder_node.inputs.x = 1\n", - "adder_node.outputs.sum.value # We use `value` to see the data the channel holds" + "adder_node.outputs.sum_.value # We use `value` to see the data the channel holds" ] }, { @@ -191,7 +213,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "id": "dc41a447-15fd-4df2-b60a-0935d81d469e", "metadata": {}, "outputs": [ @@ -201,14 +223,14 @@ "2" ] }, - "execution_count": 7, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "adder_node.run()\n", - "adder_node.outputs.sum.value" + "adder_node.outputs.sum_.value" ] }, { @@ -222,7 +244,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "id": "ac0fe993-6c82-48c8-a780-cbd0c97fc386", "metadata": {}, "outputs": [ @@ -232,7 +254,7 @@ "(int, str)" ] }, - "execution_count": 8, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -254,7 +276,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "id": "bcbd17f1-a3e4-44f0-bde1-cbddc51c5d73", "metadata": {}, "outputs": [ @@ -264,13 +286,13 @@ "2" ] }, - "execution_count": 9, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "adder_node.outputs.sum.value" + "adder_node.outputs.sum_.value" ] }, { @@ -283,7 +305,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, "id": "15742a49-4c23-4d4a-84d9-9bf19677544c", "metadata": {}, "outputs": [ @@ -293,14 +315,14 @@ "3" ] }, - "execution_count": 10, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "adder_node.inputs.x.update(2)\n", - "adder_node.outputs.sum.value" + "adder_node.outputs.sum_.value" ] }, { @@ -319,7 +341,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 12, "id": "61b43a9b-8dad-48b7-9194-2045e465793b", "metadata": {}, "outputs": [], @@ -329,7 +351,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "id": "647360a9-c971-4272-995c-aa01e5f5bb83", "metadata": {}, "outputs": [ @@ -344,9 +366,10 @@ } ], "source": [ - "@function_node(\"diff\")\n", + "@function_node()\n", "def subtract_node(x: int | float = 2, y: int | float = 1) -> int | float:\n", - " return x - y\n", + " diff = x - y\n", + " return diff\n", "\n", "sn = subtract_node()\n", "print(\"class name =\", sn.__class__.__name__)\n", @@ -366,7 +389,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 14, "id": "8fb0671b-045a-4d71-9d35-f0beadc9cf3a", "metadata": {}, "outputs": [ @@ -376,7 +399,7 @@ "-10" ] }, - "execution_count": 13, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -397,7 +420,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 15, "id": "5ce91f42-7aec-492c-94fb-2320c971cd79", "metadata": {}, "outputs": [ @@ -410,15 +433,16 @@ } ], "source": [ - "@function_node(\"sum\")\n", + "@function_node()\n", "def add_node(x: int | float = 1, y: int | float = 1) -> int | float:\n", - " return x + y\n", + " sum_ = x + y\n", + " return sum_\n", "\n", "add1 = add_node()\n", "add2 = add_node(x=2, y=2)\n", - "sub = subtract_node(x=add1.outputs.sum, y=add2.outputs.sum)\n", + "sub = subtract_node(x=add1.outputs.sum_, y=add2.outputs.sum_)\n", "print(\n", - " f\"{add1.outputs.sum.value} - {add2.outputs.sum.value} = {sub.outputs.diff.value}\"\n", + " f\"{add1.outputs.sum_.value} - {add2.outputs.sum_.value} = {sub.outputs.diff.value}\"\n", ")" ] }, @@ -432,7 +456,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 16, "id": "20360fe7-b422-4d78-9bd1-de233f28c8df", "metadata": {}, "outputs": [ @@ -447,7 +471,7 @@ "source": [ "add1.inputs.x = 10\n", "print(\n", - " f\"{add1.outputs.sum.value} - {add2.outputs.sum.value} = {sub.outputs.diff.value}\"\n", + " f\"{add1.outputs.sum_.value} - {add2.outputs.sum_.value} = {sub.outputs.diff.value}\"\n", ")" ] }, @@ -465,18 +489,19 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 17, "id": "1a4e9693-0980-4435-aecc-3331d8b608dd", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", + "\n", "from pyiron_contrib.workflow.function import single_value_node" ] }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 18, "id": "7c4d314b-33bb-4a67-bfb9-ed77fba3949c", "metadata": {}, "outputs": [ @@ -491,17 +516,18 @@ } ], "source": [ - "@single_value_node(\"linspace\")\n", + "@single_value_node()\n", "def linspace_node(\n", " start: int | float = 0, stop: int | float = 1, num: int = 50\n", "):\n", - " return np.linspace(start=start, stop=stop, num=num)\n", + " linspace = np.linspace(start=start, stop=stop, num=num)\n", + " return linspace\n", "\n", "lin = linspace_node()\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", - "print(lin.mean()) # Finds the method on the output" + "print(lin.mean()) # Finds the method on the output -- a special feature of SingleValueNode" ] }, { @@ -512,19 +538,21 @@ "# Workflows\n", "\n", "Typically, you will have a group of nodes working together with their connections.\n", - "We call these groups workflows, and offer a `Workflow(Node)` object as a single point of entry -- 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." + "We call these groups workflows, and offer a `Workflow(Node)` object as a single point of entry -- 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 can also rename our node output channels using the `output_labels: Optional[str | list[str] | tuple[str]` kwarg, which we'll see here" ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 19, "id": "1cd000bd-9b24-4c39-9cac-70a3291d0660", "metadata": {}, "outputs": [], "source": [ "from pyiron_contrib.workflow.workflow import Workflow\n", "\n", - "@Workflow.wrap_as.single_value_node(\"is_greater\")\n", + "@Workflow.wrap_as.single_value_node(output_labels=\"is_greater\")\n", "def greater_than_half(x: int | float | bool = 0) -> bool:\n", " \"\"\"The functionality doesn't matter here, it's just an example\"\"\"\n", " return x > 0.5" @@ -542,7 +570,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 20, "id": "7964df3c-55af-4c25-afc5-9e07accb606a", "metadata": {}, "outputs": [ @@ -563,7 +591,7 @@ "n1 = greater_than_half(label=\"n1\")\n", "\n", "wf = Workflow(\"my_wf\", n1) # As args at init\n", - "wf.add.Slow(lambda: x + 1, \"p1\", label=\"n2\") # Instantiating from the class with a lambda function\n", + "wf.add.Slow(lambda: x + 1, output_labels=\"p1\", label=\"n2\") # Instantiating from the class with a lambda function\n", "# (Slow since we don't have an x default)\n", "wf.add(greater_than_half(label=\"n3\")) # Instantiating then passing to node adder\n", "wf.n4 = greater_than_half(label=\"will_get_overwritten_with_n4\") # Set attribute to instance\n", @@ -591,7 +619,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 21, "id": "2e418abf-7059-4e1e-9b9f-b3dc0a4b5e35", "metadata": {}, "outputs": [ @@ -604,17 +632,18 @@ } ], "source": [ - "@function_node(\"y\")\n", + "@function_node()\n", "def linear(x):\n", " return x\n", "\n", - "@function_node(\"z\", run_on_updates=False)\n", - "def times_two(y):\n", - " return 2 * y\n", + "@function_node(run_on_updates=False)\n", + "def times_two(x):\n", + " double = 2 * x\n", + " return double\n", "\n", "l = linear(x=1)\n", - "t2 = times_two(y=l.outputs.y)\n", - "print(t2.inputs.y, t2.outputs.z)" + "t2 = times_two(x=l.outputs.x)\n", + "print(t2.inputs.x, t2.outputs.double)" ] }, { @@ -631,7 +660,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 22, "id": "3310eac4-04f6-421b-9824-19bb2d680be6", "metadata": {}, "outputs": [ @@ -644,7 +673,7 @@ } ], "source": [ - "@function_node(\"void\")\n", + "@function_node()\n", "def control():\n", " return\n", "\n", @@ -652,7 +681,15 @@ "l.signals.input.run = c.signals.output.ran\n", "t2.signals.input.run = l.signals.output.ran\n", "c.run()\n", - "print(t2.outputs.z.value)" + "print(t2.outputs.double.value)" + ] + }, + { + "cell_type": "markdown", + "id": "003ed16e-c493-4465-9f08-492f9c51f764", + "metadata": {}, + "source": [ + "`Function` and its children always push out data updates _before_ triggering their `ran` signal." ] }, { @@ -665,7 +702,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 23, "id": "7a6f2bce-6b5e-4321-9457-0a6790d2202a", "metadata": {}, "outputs": [], @@ -675,13 +712,13 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 24, "id": "6569014a-815b-46dd-8b47-4e1cd4584b3b", "metadata": {}, "outputs": [ { "data": { - "image/png": "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", + "image/png": "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DBs2zDz55JMRnnxgnK67y+HDh83pp59uioqKzHnnnReZYUPI6brfeustI8l89NFHxufzdb+OHTsW4ckHJpjf909/+lMzadIkU11dberr6827775rtm7dGsGpB8bpmg8fPhzwO25sbDRpaWmmtLQ0soMPkNN1b9682cTFxZlHH33U7N2712zevNmcc845ZubMmRGefGCcrruiosKkpKSYF1980XzyySdm9erV5qSTTjLr1q2L8OSRETUxkp+fb0pKSgK2nX322Wbx4sX9/h45OTmmrKws1KOFVSjW/fOf/9zMmjUr1KOFVbDrLi4uNn/4wx9MaWlpVMaI03V3xch///vfCEwXPk7X/frrrxuPx2MOHjwYifHCYqD/bq9du9a4XC7z6aefhmO8sHG67vvvv9+cfvrpAdsee+wxk5mZGbYZw8HpugsKCswdd9wRsO22224zkydPDtuMNkXFZZqjR4+qpqZGRUVFAduLioq0bdu2fn2Pzs5Otba2Ki0tLRwjhkUo1r1r1y5t27ZNF198cThGDItg1/3MM8/ok08+UWlpabhHDIuB/L4vuOACeb1eXXbZZXrrrbfCOWbIBbPudevWaeLEibrvvvs0evRonXXWWbrjjjv09ddfR2LkAQvFv9srVqzQ5ZdfrjFjxoRjxLAIZt2FhYXat2+fNmzYIGOMvvjiC7388su66qqrIjFySASz7vb2drnd7oBtycnJ2r59u7755puwzWpLVMTIgQMH1NHRofT09IDt6enpampq6tf3ePDBB/XVV1/pmmuuCceIYTGQdWdmZiopKUkTJ07UzTffrPnz54dz1JAKZt0ff/yxFi9erFWrVikhISr+Muoeglm31+vVU089pVdeeUWvvvqqxo0bp8suu0xvv/12JEYOiWDWvXfvXm3ZskW1tbVau3atHnnkEb388su6+eabIzHygA30v2k+n0+vv/56VP17LQW37sLCQq1atUrFxcVKTExURkaGTj75ZD3++OORGDkkgln3FVdcoaefflo1NTUyxmjHjh2qqqrSN998owMHDkRi7IiKqv9qu1yugK+NMT229Wb16tW6++679dprr2nkyJHhGi9sgln35s2b9eWXX+qdd97R4sWLdcYZZ+iXv/xlOMcMuf6uu6OjQ9ddd53Kysp01llnRWq8sHHy+x43bpzGjRvX/XVBQYEaGxv1wAMP6H/+53/COmeoOVl3Z2enXC6XVq1a1f3Xkz/00EO6+uqrtXz5ciUnJ4d93lAI9r9pzz77rE4++WTNnDkzTJOFl5N119XV6dZbb9Vdd92lK664Qj6fT3feeadKSkq0YsWKSIwbMk7W/cc//lFNTU268MILZYxRenq65syZo/vuu0/x8fGRGDeiouLMyIgRIxQfH9+jIJubm3uU5netWbNG8+bN01/+8hddfvnl4Rwz5Aay7uzsbP34xz/WjTfeqNtvv1133313GCcNLafrbm1t1Y4dO3TLLbcoISFBCQkJWrZsmf79738rISFBf//73yM1+oAM5Pf9bRdeeKE+/vjjUI8XNsGs2+v1avTo0d0hIknjx4+XMUb79u0L67yhMJDftTFGVVVVuv7665WYmBjOMUMumHWXl5dr8uTJuvPOO3XuuefqiiuuUEVFhaqqquTz+SIx9oAFs+7k5GRVVVXpyJEj+vTTT9XQ0KDTTjtNKSkpGjFiRCTGjqioiJHExETl5eWpuro6YHt1dbUKCwv7fN/q1as1Z84cvfDCC1F1fbFLsOv+LmOM2tvbQz1e2Dhdd2pqqt577z3t3r27+1VSUqJx48Zp9+7dmjRpUqRGH5BQ/b537dolr9cb6vHCJph1T548Wfv379eXX37ZvW3Pnj2Ki4tTZmZmWOcNhYH8rjdt2qT//Oc/mjdvXjhHDItg1n3kyBHFxQUeqrrODJgo+XteB/L7PuGEE5SZman4+Hi9+OKL+slPftLjf48hwcKHZoPSdVvUihUrTF1dnVm4cKE58cQTuz9JvnjxYnP99dd37//CCy+YhIQEs3z58oDb4Q4fPmxrCUFxuu4nnnjCrFu3zuzZs8fs2bPHVFVVmdTUVLN06VJbSwiK03V/V7TeTeN03Q8//LBZu3at2bNnj6mtrTWLFy82kswrr7xiawlBcbru1tZWk5mZaa6++mrz/vvvm02bNpkzzzzTzJ8/39YSHAv2n/FZs2aZSZMmRXrckHG67meeecYkJCSYiooK88knn5gtW7aYiRMnmvz8fFtLCIrTdX/00Ufm+eefN3v27DHvvvuuKS4uNmlpaaa+vt7SCsIramLEGGOWL19uxowZYxITE82ECRPMpk2buv/shhtuMBdffHH31xdffLGR1ON1ww03RH7wAXKy7scee8ycc845ZtiwYSY1NdVccMEFpqKiwnR0dFiYfGCcrPu7ojVGjHG27nvvvdeMHTvWuN1u86Mf/chcdNFF5m9/+5uFqQfO6e/7gw8+MJdffrlJTk42mZmZZtGiRebIkSMRnnpgnK758OHDJjk52Tz11FMRnjS0nK77scceMzk5OSY5Odl4vV7zq1/9yuzbty/CUw+ck3XX1dWZ888/3yQnJ5vU1FTzs5/9zHz44YcWpo4MlzFRcp4LAAAMSUPwwhMAAIgmxAgAALCKGAEAAFYRIwAAwCpiBAAAWEWMAAAAq4gRAABgFTECAACsIkYAAIBVxAgAALCKGAEAAFb9LzGeNuwnas6eAAAAAElFTkSuQmCC", "text/plain": [ "
" ] @@ -691,13 +728,15 @@ } ], "source": [ - "@single_value_node(\"array\")\n", + "@single_value_node()\n", "def noise(length: int = 1):\n", - " return np.random.rand(length)\n", + " array = np.random.rand(length)\n", + " return array\n", "\n", - "@function_node(\"fig\")\n", + "@function_node()\n", "def plot(x, y):\n", - " return plt.scatter(x, y)\n", + " fig = plt.scatter(x, y)\n", + " return fig\n", "\n", "x = noise(length=10)\n", "y = noise(length=10)\n", @@ -719,7 +758,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 25, "id": "25f0495a-e85f-43b7-8a70-a2c9cbd51ebb", "metadata": {}, "outputs": [ @@ -729,7 +768,7 @@ "(False, False)" ] }, - "execution_count": 24, + "execution_count": 25, "metadata": {}, "output_type": "execute_result" } @@ -740,13 +779,35 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 26, "id": "449ce797-be62-4211-b483-c717a3d70583", "metadata": {}, "outputs": [ { "data": { - "image/png": "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iooKHT58OORz3nrrLS1cuFBPPvmkZs+erfnz5+vhhx/WN998M+H7DA8Pa2BgIOgBAADC40xxqLqyUNJI8Djf6M/VlYXG9huxFUbOnDkjv9+vnJycoPacnBz19PSEfM6JEyd08OBBdXR06M0339SuXbt04MAB3X///RO+T21trdxu99gjLy/PTpkAAOACy4s8ql9drFx38FRMrtul+tXFRvcZCWvTM4cjODlZljWubVQgEJDD4dC+ffvkdrslSTt37tTtt9+u3bt3a9q0aeOes23bNm3ZsmXs54GBAQIJAACXaXmRR8sKc+N7B9aZM2fK6XSOGwXp7e0dN1oyyuPxaPbs2WNBRBq5x8SyLJ06dUrz5s0b95z09HSlp6fbKQ0AAEyCM8WhsrkzTJcRxNY0TVpamkpKStTS0hLU3tLSovLy8pDPWbx4sU6fPq0vv/xyrO2TTz5RSkqK5syZE0bJAAAgkdjeZ2TLli168cUXtWfPHn388cd66KGH1N3drQ0bNkgamWJZu3btWP8777xTM2bM0D333KPOzk699957euSRR3TvvfeGnKIBAADJxfY9I1VVVerr69OOHTvk9XpVVFSkpqYm5efnS5K8Xq+6u7vH+n/rW99SS0uLfvnLX2rhwoWaMWOGVq1apccff3zq/goAABC3bO8zYgL7jAAAEH8iss8IAADAVCOMAAAAowgjAADAKMIIAAAwijACAACMIowAAACjCCMAAMAowggAADCKMAIAAIwijAAAAKMIIwAAwCjCCAAAMIowAgAAjCKMAAAAowgjAADAKMIIAAAwijACAACMIowAAACjCCMAAMAowggAADCKMAIAAIwijAAAAKMIIwAAwCjCCAAAMIowAgAAjCKMAAAAowgjAADAKMIIAAAwijACAACMIowAAACjCCMAAMAowggAADCKMAIAAIwijAAAAKMIIwAAwCjCCAAAMIowAgAAjCKMAAAAowgjAADAKMIIAAAwijACAACMIowAAACjCCMAAMAowggAADCKMAIAAIwijAAAAKNSTRcAAIhf/oCltq5+9Q4OKTvDpdKCLDlTHKbLQpwhjAAAwtLc4VVNY6e8vqGxNo/bperKQi0v8hisDPGGaRoAgG3NHV5t3Hs0KIhIUo9vSBv3HlVzh9dQZYhHhBEAgC3+gKWaxk5ZIX432lbT2Cl/IFQPYDzCCADA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", + "text/plain": [ + "(True, False)" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x.inputs.length = 20\n", + "f.inputs.x.ready, f.inputs.y.ready" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "7008b0fc-3644-401c-b49f-9c40f9d89ac4", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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X7jXZtiXX35pZs2aZcrLy82VV14Zn2JTTCTX5pZbbjHz5s0zDQ0NprW11fzzn/80e/fujWPVqSHca7N7926TlpZmnnnmGXPs2DGze/duc/nll5vFixfHuXJnq6urM+vWrTNvvfWWkWTeeeedsx5/7Ngxc8EFF5iHHnrItLS0mJdeesmMGzfOvPnmm2G9L4HFQUpKSkxlZWVA22WXXWYeffTRkF+joKDAVFdXR7u0lBfptVmyZIn57W9/a6qqqggsMRLutfnLX/5iPB6P6erqikd5KS3ca/O73/3OzJo1K6Dt2WefNdOnT49ZjakulMDyq1/9ylx22WUBbffdd5+5+uqrw3ovhoQc4uTJkzp48KDKy8sD2svLy7Vv376QXmNwcFC9vb3Kzs6ORYkpK9Jr88orr+jo0aOqqqqKdYkpK5Jr895772nu3Ll66qmnNG3aNF1yySV6+OGH9e2338aj5JQRybUpKyvTiRMnVFdXJ2OMvvjiC7355pu6+eab41EyxrB///6g63jjjTfqwIED+u6770J+naS4WzPOrbOzUwMDA8rNzQ1oz83NVXt7e0iv8fvf/15ff/21brvttliUmLIiuTaffPKJHn30Ue3evVsZGXxMYyWSa3Ps2DHt2bNHbrdb77zzjjo7O3X//feru7ubeSxRFMm1KSsr09atW7VkyRL19fXp1KlTuuWWW/Tcc8/Fo2SMob29fdTreOrUKXV2dsrr9Yb0OvSwOIzL5Qp4bowJahvNtm3b9Pjjj2v79u2aMmVKrMpLaaFem4GBAd1xxx2qrq7WJZdcEq/yUlo4n5vBwUG5XC5t3bpVJSUluummm/T000/r1VdfpZclBsK5Ni0tLVq5cqUee+wxHTx4UPX19WptbVVlZWU8SsVZjHYdR2s/G351c4jJkycrPT096DePjo6OoGR7pu3bt+uee+7RG2+8oRtuuCGWZaakcK9Nb2+vDhw4oKamJj344IOShr4kjTHKyMjQBx98oOuvvz4utTtdJJ8br9eradOmyePxjLTNmTNHxhidOHFCF198cUxrThWRXJuamhpdc801euSRRyRJV1xxhSZOnKj58+friSeeCPk3eURXXl7eqNcxIyNDOTk5Ib8OPSwOMX78eBUXF6uhoSGgvaGhQWVlZWOet23bNt111116/fXXGeeNkXCvTVZWlj788EM1NzePPCorK3XppZequblZ8+bNi1fpjhfJ5+aaa67R559/rq+++mqk7eOPP1ZaWpqmT58e03pTSSTX5ptvvlFaWuDXWnp6uqT//UaP+CstLQ26jh988IHmzp2rcePGhf5CYU3RhdWGlwBu2rTJtLS0mFWrVpmJEyea//73v8YYYx599FFz5513jhz/+uuvm4yMDLNhwwbj8/lGHl9++WWifgTHCvfanIlVQrET7rXp7e0106dPN7feeqv56KOPzM6dO83FF19s7r333kT9CI4V7rV55ZVXTEZGhtm4caM5evSo2bNnj5k7d64pKSlJ1I/gSL29vaapqck0NTUZSebpp582TU1NI8vNz7wuw8uaV69ebVpaWsymTZtY1gxjNmzYYGbOnGnGjx9vioqKzM6dO0f+bPny5ebaa68deX7ttdcaSUGP5cuXx7/wFBDOtTkTgSW2wr02R44cMTfccIOZMGGCmT59ulmzZo355ptv4lx1agj32jz77LOmoKDATJgwwXi9XvOzn/3MnDhxIs5VO9vf/va3s353jHZd/v73v5sf/vCHZvz48eaiiy4ytbW1Yb+vyxj6yQAAgN2YwwIAAKxHYAEAANYjsAAAAOsRWAAAgPUILAAAwHoEFgAAYD0CCwAAsB6BBQAAWI/AAgAArEdgAQAA1iOwAAAA6/0/dvCXVJIj9t8AAAAASUVORK5CYII=", "text/plain": [ "
" ] @@ -756,7 +817,6 @@ } ], "source": [ - "x.inputs.length = 20\n", "y.inputs.length = 20" ] }, @@ -784,7 +844,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 28, "id": "ae500d5e-e55b-432c-8b5f-d5892193cdf5", "metadata": {}, "outputs": [ @@ -792,9 +852,9 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_contrib/pyiron_contrib/workflow/composite.py:177: 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:193: 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:177: 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:193: UserWarning: Reassigning the node lammps to the label engine when adding it to the parent with_prebuilt.\n", " warn(\n" ] }, @@ -802,22 +862,22 @@ "name": "stdout", "output_type": "stream", "text": [ - "The job JUSTAJOBNAME was saved and received the ID: 9553\n" + "The job JUSTAJOBNAME was saved and received the ID: 9558\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_contrib/pyiron_contrib/workflow/composite.py:177: 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:193: 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:177: 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:193: UserWarning: Reassigning the node scatter to the label plot when adding it to the parent with_prebuilt.\n", " warn(\n" ] }, { "data": { - "image/png": "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", + "image/png": "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d7vd8nq9re4jFApp9erVWrdunUaNGiVJKi4uVk5OjrZs2aKxY8dGewwAAKCH6dCbZEOhkCQpLS1y1tytW7cqMzNTl112mYYNG6annnpKmZmZkqTy8nKdOHFCY8aMceqzs7Pl9/u1Y8eOVgNKOBxWOBw+43vr6uo60nwAANCFTp23jTHnrG13QDHG6JFHHtFtt90mv9/vrB8/frzuuusu5ebmqqqqSr/4xS80YsQIlZeXy+12KxgMKjExUX369InYX1ZWloLBYKvfVVRUpGXLlp2xPicnp73NBwAA3aS+vl4ej+esNe0OKA8//LD+/Oc/a/v27RHrp02b5vzZ7/dr4MCBys3N1dtvv60pU6a0uT9jjFyu1ie5W7x4sR555BHn55aWFh09elTp6eltfqa96urqlJOTo+rqaqWmpnbqvvH/6OeuQT93Dfq569DXXeNC9bMxRvX19crOzj5nbbsCyrx58/TWW2/pgw8+0BVXXHHWWp/Pp9zcXB06dEiS5PV61dTUpNra2ohRlJqaGg0dOrTVfbjdbrnd7oh1l112WXuaft5SU1P5x98F6OeuQT93Dfq569DXXeNC9PO5Rk5OieopHmOMHn74Yb355pt6//33lZeXd87PHDlyRNXV1fL5fJKkgoICJSQkqKyszKkJBAKqrKxsM6AAAICLS1QjKHPnztX69ev1+9//XikpKc49Ix6PR7169VJDQ4OWLl2qO++8Uz6fT59++qmWLFmijIwM3XHHHU7t7NmztWDBAqWnpystLU0LFy5Ufn6+81QPAAC4uEUVUFauXClJGj58eMT6V199VbNmzVJcXJwqKiq0du1aHTt2TD6fT7fffrveeOMNpaSkOPUrVqxQfHy8pk6dqsbGRo0cOVJr1qxRXFxcx4+og9xut5544okzLimhc9HPXYN+7hr0c9ehr7uGDf3sMufzrA8AAEAXYi4eAABgHQIKAACwDgEFAABYh4ACAACsQ0D5O//+7/+uvLw8JSUlqaCgQH/84x+7u0kxpaioSDfffLNSUlKUmZmpH//4x/roo48iaowxWrp0qbKzs9WrVy8NHz5cBw4ciKgJh8OaN2+eMjIylJycrMmTJ+vw4cNdeSgxpaioSC6XS4WFhc46+rlzfPHFF5oxY4bS09PVu3dv3XjjjSovL3e2088d99133+lf//VflZeXp169eumqq67S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", "text/plain": [ "
" ] From 58801249ab73c00fc2322af58c27a60ef5c2723e Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 14 Jul 2023 13:56:03 -0700 Subject: [PATCH 19/59] Let specified labels totally override scraping This way we have a route for handling cases scraping can't (like multiple return branches), but it comes at the user/node designer assuming that risk themself. --- pyiron_contrib/workflow/function.py | 39 ++++++++++++++++------------ tests/unit/workflow/test_function.py | 20 +++++++++++--- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/pyiron_contrib/workflow/function.py b/pyiron_contrib/workflow/function.py index e799e715f..d6211d948 100644 --- a/pyiron_contrib/workflow/function.py +++ b/pyiron_contrib/workflow/function.py @@ -78,7 +78,10 @@ class Function(Node): that requires item-string-based access. Additionally, specifying a _single_ label for a wrapped function that returns a tuple of values ensures that a _single_ output channel (holding the tuple) is created, instead of one - channel for each return value. + channel for each return value. The default approach of extracting labels + from the function source code also requires that the function body contain + _at most_ one `return` expression, so providing explicit labels can be used + to circumvent this (at your own risk). **kwargs: Any additional keyword arguments whose keyword matches the label of an input channel will have their value assigned to that channel. @@ -361,26 +364,30 @@ def __init__( def _get_output_labels(self, output_labels: str | list[str] | tuple[str] | None): """ - Explicitly passed output labels can be used to rename awkward parsed labels, or - to force the creation of a _single_ output channel when wrapped functions return - a tuple of values. + If output labels are provided, turn convert them to a list if passed as a + string and return them, else scrape them from the source channel. + + Note: When the user explicitly provides output channels, they are taking + responsibility that these are correct, e.g. in terms of quantity, order, etc. """ - parsed_labels = ParseOutput(self.node_function).output if output_labels is None: - return parsed_labels if parsed_labels is not None else [] + return self._scrape_output_labels() + elif isinstance(output_labels, str): + return [output_labels] else: - if isinstance(output_labels, str): - output_labels = (output_labels,) + return output_labels - if len(output_labels) != 1 and len(output_labels) != len(parsed_labels): - raise ValueError( - f"When output labels are explicitly provided they must either be a " - f"_single_ label, or match the length of the parsed labels. In " - f"this case, {output_labels} were received while {parsed_labels} " - f"were parsed." - ) + def _scrape_output_labels(self): + """ + Inspect the source code to scrape out strings representing the returned values. + _Only_ works for functions with a single `return` expression in their body. - return output_labels + Will return expressions and function calls just fine, thus best practice is to + create well-named variables and return those so that the output labels stay + dot-accessible. + """ + parsed_outputs = ParseOutput(self.node_function).output + return [] if parsed_outputs is None else parsed_outputs @property def _input_args(self): diff --git a/tests/unit/workflow/test_function.py b/tests/unit/workflow/test_function.py index dac78dd1d..914d4d239 100644 --- a/tests/unit/workflow/test_function.py +++ b/tests/unit/workflow/test_function.py @@ -26,14 +26,22 @@ def no_default(x, y): def returns_multiple(x, y): return x, y, x + y + def void(): pass +def multiple_branches(x): + if x < 10: + return True + else: + return False + + @unittest.skipUnless(version_info[0] == 3 and version_info[1] >= 10, "Only supported for 3.10+") class TestFunction(unittest.TestCase): def test_instantiation(self): - with self.subTest("Void function"): + with self.subTest("Void function is allowable"): void_node = Function(void) self.assertEqual(len(void_node.outputs), 0) @@ -71,9 +79,15 @@ def test_label_choices(self): n = Function(returns_multiple, output_labels="its_a_tuple") self.assertListEqual(n.outputs.labels, ["its_a_tuple"]) - with self.subTest("Force matching lengths"): + with self.subTest("Fail on multiple return values"): with self.assertRaises(ValueError): - Function(returns_multiple, output_labels=["one", "two"]) + # Can't automatically parse output labels from a function with multiple + # return expressions + Function(multiple_branches) + + with self.subTest("Override output label scraping"): + switch = Function(multiple_branches, output_labels="bool") + self.assertListEqual(switch.outputs.labels, ["bool"]) def test_instantiation_update(self): no_update = Function( From 2c278020e6118b88dc0e0f3a99b6d97bcc9ffbd1 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 14 Jul 2023 14:16:41 -0700 Subject: [PATCH 20/59] Update function docs --- pyiron_contrib/workflow/function.py | 83 ++++++++++++++++++----------- 1 file changed, 53 insertions(+), 30 deletions(-) diff --git a/pyiron_contrib/workflow/function.py b/pyiron_contrib/workflow/function.py index d6211d948..7ff972963 100644 --- a/pyiron_contrib/workflow/function.py +++ b/pyiron_contrib/workflow/function.py @@ -19,8 +19,9 @@ class Function(Node): """ Function nodes wrap an arbitrary python function. - Node IO, including type hints, is generated automatically from the provided function - and (in the case of labeling output channels) the provided output labels. + Node IO, including type hints, is generated automatically from the provided + function. + On running, the function node executes this wrapped function with its current input and uses the results to populate the node output. @@ -30,16 +31,20 @@ class Function(Node): is currently no way to mix-and-match, i.e. to have multiple return values at least one of which is a tuple.) - The node label (unless otherwise provided), IO types, and input defaults for the - node are produced _automatically_ from introspection of the node function. - Additional properties like storage priority (present but doesn't do anything yet) - and ontological type (not yet present) can be set using kwarg dictionaries with - keys corresponding to the channel labels (i.e. the node arguments of the node - function, or the output labels provided). + The node label (unless otherwise provided), IO channel names, IO types, and input + defaults for the node are produced _automatically_ from introspection of the node + function. + Explicit output labels can be provided to modify the number of return values (from + $N$ to 1 in case you _want_ a tuple returned) and to dodge constraints on the + automatic scraping routine (namely, that there be _at most_ one `return` + expression). + (Additional properties like storage priority and ontological type are forthcoming + as kwarg dictionaries with keys corresponding to the channel labels (i.e. the node + arguments of the node function, or the output labels provided).) Actual function node instances can either be instances of the base node class, in - which case the callable node function and output labels *must* be provided, in - addition to other data, OR they can be instances of children of this class. + which case the callable node function *must* be provided OR they can be instances + of children of this class. 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. @@ -48,6 +53,8 @@ class Function(Node): nodes should be both functional (always returning the same output given the same input) and idempotent (not modifying input data in-place, but creating copies where necessary and returning new objects as output). + Further, functions with multiple return branches that return different types or + numbers of return values may or may not work smoothly, depending on the details. By default, function nodes will attempt to run whenever one or more inputs is updated, and will attempt to update on initialization (after setting _all_ initial @@ -55,7 +62,7 @@ class Function(Node): Output is updated in the `process_run_result` inside the parent class `finish_run` call, such that output data gets pushed after the node stops running but before - then `ran` signal fires. + then `ran` signal fires: run, process and push result, ran. Args: node_function (callable): The function determining the behaviour of the node. @@ -110,9 +117,9 @@ class Function(Node): >>> def mwe(x, y): ... return x+1, y-1 >>> - >>> plus_minus_1 = Function(mwe, "p1", "m1") + >>> plus_minus_1 = Function(mwe) >>> - >>> print(plus_minus_1.outputs.p1) + >>> print(plus_minus_1.outputs["x+1"]) There is no output because we haven't given our function any input, it has @@ -133,13 +140,17 @@ class Function(Node): Once we update `y`, all the input is ready and the automatic `update()` call will be allowed to proceed to a `run()` call, which succeeds and updates the - output: - >>> plus_minus_1.inputs.x = 3 + output. + The final thing we need to do is disable the `failed` status we got from our + last run call + >>> plus_minus_1.failed = False + >>> plus_minus_1.inputs.y = 3 >>> plus_minus_1.outputs.to_value_dict() - {'p1': 3, 'm1': 2} + {'x+1': 3, 'y-1': 2} - We can also, optionally, provide initial values for some or all of the input - >>> plus_minus_1 = Function(mwe, "p1", "m1", x=1) + We can also, optionally, provide initial values for some or all of the input and + labels for the output: + >>> plus_minus_1 = Function(mwe, output_labels=("p1", "m1"), x=1) >>> plus_minus_1.inputs.y = 2 # Automatically triggers an update call now >>> plus_minus_1.outputs.to_value_dict() {'p1': 2, 'm1': 1} @@ -147,7 +158,7 @@ class Function(Node): Finally, we might stop these updates from happening automatically, even when all the input data is present and available: >>> plus_minus_1 = Function( - ... mwe, "p1", "m1", + ... mwe, output_labels=("p1", "m1"), ... x=0, y=0, ... run_on_updates=False, update_on_instantiation=False ... ) @@ -163,7 +174,7 @@ class Function(Node): if they haven't seen any input data. However, we could still get them to raise an error by providing the _wrong_ data: - >>> plus_minus_1 = Function(mwe, "p1", "m1", x=1, y="can't add to an int") + >>> plus_minus_1 = Function(mwe, x=1, y="can't add to an int") TypeError Here everything tries to run automatically, but we get an error from adding the @@ -179,15 +190,19 @@ class Function(Node): return hint. Our treatment of type hints is **not infinitely robust**, but covers a wide variety of common use cases. + 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`: >>> from typing import Union >>> >>> def hinted_example( ... x: Union[int, float], ... y: int | float = 1 ... ) -> tuple[int, int | float]: - ... return x+1, y-1 + ... p1, m1 = x+1, y-1 + ... return p1, m1 >>> - >>> plus_minus_1 = Function(hinted_example, "p1", "m1", x="not an int") + >>> plus_minus_1 = Function(hinted_example, x="not an int") >>> plus_minus_1.outputs.to_value_dict() {'p1': , 'm1': } @@ -218,7 +233,7 @@ class Function(Node): and returns a node class: >>> from pyiron_contrib.workflow.function import function_node >>> - >>> @function_node("p1", "m1") + >>> @function_node(output_labels=("p1", "m1")) ... def my_mwe_node( ... x: int | float, y: int | float = 1 ... ) -> tuple[int | float, int | float]: @@ -248,7 +263,6 @@ class Function(Node): ... ): ... super().__init__( ... self.alphabet_mod_three, - ... "letter", ... label=label, ... run_on_updates=run_on_updates, ... update_on_instantiation=update_on_instantiation, @@ -257,7 +271,8 @@ class Function(Node): ... ... @staticmethod ... def alphabet_mod_three(i: int) -> Literal["a", "b", "c"]: - ... return ["a", "b", "c"][i % 3] + ... letter = ["a", "b", "c"][i % 3] + ... return letter Note that we've overridden the default value for `update_on_instantiation` above. @@ -274,12 +289,12 @@ class Function(Node): >>> class Adder(Function): ... @staticmethod ... def adder(x: int = 0, y: int = 0) -> int: - ... return x + y + ... sum = x + y + ... return sum ... ... __init__ = partialmethod( ... Function.__init__, ... adder, - ... "sum", ... ) Finally, let's put it all together by using both of these nodes at once. @@ -585,6 +600,9 @@ class SingleValue(Function, HasChannel): A node that _must_ return only a single value. Attribute and item access is modified to finally attempt access on the output value. + 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). """ def __init__( @@ -645,10 +663,11 @@ def function_node(**node_class_kwargs): A decorator for dynamically creating node classes from functions. Decorates a function. - Takes an output label for each returned value of the function. - Returns a `Function` subclass whose name is the camel-case version of the function node, - and whose signature is modified to exclude the node function and output labels + Returns a `Function` subclass whose name is the camel-case version of the function + node, and whose signature is modified to exclude the node function and output labels (which are explicitly defined in the process of using the decorator). + + Optionally takes any keyword arguments of `Function`. """ def as_node(node_function: callable): @@ -674,6 +693,8 @@ def slow_node(**node_class_kwargs): Unlike normal nodes, slow nodes do update themselves on initialization and do not run themselves when they get updated -- i.e. they will not run when their input changes, `run()` must be explicitly called. + + Optionally takes any keyword arguments of `Slow`. """ def as_slow_node(node_function: callable): @@ -697,6 +718,8 @@ def single_value_node(**node_class_kwargs): A decorator for dynamically creating fast node classes from functions. Unlike normal nodes, fast nodes _must_ have default values set for all their inputs. + + Optionally takes any keyword arguments of `SingleValueNode`. """ def as_single_value_node(node_function: callable): From be6485c1cd11088fcd48a3b6c4b0f9e0b96c18d2 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 14 Jul 2023 14:20:39 -0700 Subject: [PATCH 21/59] Update workflow docs --- pyiron_contrib/workflow/workflow.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/pyiron_contrib/workflow/workflow.py b/pyiron_contrib/workflow/workflow.py index 9ce81342e..8c9f7936d 100644 --- a/pyiron_contrib/workflow/workflow.py +++ b/pyiron_contrib/workflow/workflow.py @@ -34,16 +34,17 @@ class Workflow(Composite): >>> from pyiron_contrib.workflow.workflow import Workflow >>> from pyiron_contrib.workflow.function import Function >>> - >>> def fnc(x=0): return x + 1 + >>> def fnc(x=0): + ... return x + 1 >>> - >>> n1 = Function(fnc, "x", label="n1") + >>> n1 = Function(fnc, label="n1") >>> >>> wf = Workflow("my_workflow", n1) # As *args at instantiation - >>> wf.add(Function(fnc, "x", label="n2")) # Passing a node to the add caller - >>> wf.add.Function(fnc, "y", label="n3") # Instantiating from add - >>> wf.n4 = Function(fnc, "y", label="whatever_n4_gets_used") + >>> wf.add(Function(fnc, label="n2")) # Passing a node to the add caller + >>> wf.add.Function(fnc, label="n3") # Instantiating from add + >>> wf.n4 = Function(fnc, label="whatever_n4_gets_used") >>> # By attribute assignment - >>> Function(fnc, "x", label="n5", parent=wf) + >>> Function(fnc, label="n5", parent=wf) >>> # By instantiating the node with a workflow By default, the node naming scheme is strict, so if you try to add a node to a @@ -51,10 +52,10 @@ class Workflow(Composite): at instantiation with the `strict_naming` kwarg, or afterwards by assigning a bool to this property. When deactivated, repeated assignments to the same label just get appended with an index: - >>> wf.deactivate_strict_naming() - >>> wf.my_node = Function(fnc, "y", x=0) - >>> wf.my_node = Function(fnc, "y", x=1) - >>> wf.my_node = Function(fnc, "y", x=2) + >>> wf.strict_naming = False + >>> wf.my_node = Function(fnc, x=0) + >>> wf.my_node = Function(fnc, x=1) + >>> wf.my_node = Function(fnc, x=2) >>> print(wf.my_node.inputs.x, wf.my_node0.inputs.x, wf.my_node1.inputs.x) 0, 1, 2 @@ -63,7 +64,7 @@ class Workflow(Composite): workflow (cf. the `Node` docs for more detail on the node types). Let's use these to explore a workflow's input and output, which are dynamically generated from the unconnected IO of its nodes: - >>> @Workflow.wrap_as.function_node("y") + >>> @Workflow.wrap_as.function_node(output_labels="y") >>> def plus_one(x: int = 0): ... return x + 1 >>> From 0d105f7c3cea14b63d5fcfedf99e84a823d3ac81 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 14 Jul 2023 14:23:07 -0700 Subject: [PATCH 22/59] Patch env files with executor dependence --- .ci_support/environment.yml | 1 + setup.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/.ci_support/environment.yml b/.ci_support/environment.yml index 8adc737f5..9e6f710a7 100644 --- a/.ci_support/environment.yml +++ b/.ci_support/environment.yml @@ -2,6 +2,7 @@ channels: - conda-forge dependencies: - ase =3.22.1 +- cloudpickle - coveralls - coverage - codacy-coverage diff --git a/setup.py b/setup.py index f28b0caa0..9813be8c5 100644 --- a/setup.py +++ b/setup.py @@ -44,6 +44,9 @@ 'pyiron_atomistics==0.3.0', 'pycp2k==0.2.2', ], + 'executors': [ + 'cloudpickle', + ], 'fenics': [ 'fenics==2019.1.0', 'mshr==2019.1.0', @@ -54,6 +57,7 @@ 'moto==4.1.12' ], 'workflow': [ + 'cloudpickle', 'python>=3.10', 'ipython', 'typeguard==4.0.0' From dd3e0fe0ac779a19373de6047888c412326ad715 Mon Sep 17 00:00:00 2001 From: pyiron-runner Date: Fri, 14 Jul 2023 21:23:44 +0000 Subject: [PATCH 23/59] Update env file --- .binder/environment.yml | 1 + docs/environment.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.binder/environment.yml b/.binder/environment.yml index 855c200c2..95475d32e 100644 --- a/.binder/environment.yml +++ b/.binder/environment.yml @@ -2,6 +2,7 @@ channels: - conda-forge dependencies: - ase =3.22.1 +- cloudpickle - coveralls - coverage - codacy-coverage diff --git a/docs/environment.yml b/docs/environment.yml index 79e6a9c23..9623164f9 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -4,6 +4,7 @@ dependencies: - ipykernel - nbsphinx - ase =3.22.1 +- cloudpickle - coveralls - coverage - codacy-coverage From 760c3c3d950728aecb51ccf5e4037c94d321b0c8 Mon Sep 17 00:00:00 2001 From: pyiron-runner Date: Fri, 14 Jul 2023 21:24:41 +0000 Subject: [PATCH 24/59] Format black --- pyiron_contrib/workflow/node_library/atomistics.py | 2 +- pyiron_contrib/workflow/output_parser.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyiron_contrib/workflow/node_library/atomistics.py b/pyiron_contrib/workflow/node_library/atomistics.py index fd304570e..7ff060c01 100644 --- a/pyiron_contrib/workflow/node_library/atomistics.py +++ b/pyiron_contrib/workflow/node_library/atomistics.py @@ -106,7 +106,7 @@ def calc_static( @slow_node( - output_labels= [ + output_labels=[ "cells", "displacements", "energy_pot", diff --git a/pyiron_contrib/workflow/output_parser.py b/pyiron_contrib/workflow/output_parser.py index 36342cc92..2f88e71e2 100644 --- a/pyiron_contrib/workflow/output_parser.py +++ b/pyiron_contrib/workflow/output_parser.py @@ -9,8 +9,8 @@ def _remove_spaces_until_character(string): - pattern = r'\s+(?=\s)' - modified_string = re.sub(pattern, '', string) + pattern = r"\s+(?=\s)" + modified_string = re.sub(pattern, "", string) return modified_string @@ -67,15 +67,15 @@ def get_string(self, node): for ll in range(node.lineno - 1, node.end_lineno): if ll == node.lineno - 1 == node.end_lineno - 1: string += _remove_spaces_until_character( - self.source[ll][node.col_offset:node.end_col_offset] + self.source[ll][node.col_offset : node.end_col_offset] ) elif ll == node.lineno - 1: string += _remove_spaces_until_character( - self.source[ll][node.col_offset:] + self.source[ll][node.col_offset :] ) elif ll == node.end_lineno - 1: string += _remove_spaces_until_character( - self.source[ll][:node.end_col_offset] + self.source[ll][: node.end_col_offset] ) else: string += _remove_spaces_until_character(self.source[ll]) From 6d194ffa98bcd241f534d2e504477a08fabff6e8 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 14 Jul 2023 14:54:57 -0700 Subject: [PATCH 25/59] Update integration test --- tests/integration/test_workflow.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/integration/test_workflow.py b/tests/integration/test_workflow.py index a8f2f4d58..9b018c1bb 100644 --- a/tests/integration/test_workflow.py +++ b/tests/integration/test_workflow.py @@ -16,7 +16,7 @@ def test_cyclic_graphs(self): TODO: Update once logical switches are included in the node library """ - @Workflow.wrap_as.single_value_node("rand") + @Workflow.wrap_as.single_value_node() def numpy_randint(low=0, high=20): rand = np.random.randint(low=low, high=high) print(f"Generating random number between {low} and {high}...{rand}!") @@ -29,7 +29,11 @@ class GreaterThanLimitSwitch(Function): """ def __init__(self, **kwargs): - super().__init__(self.greater_than, "value_gt_limit", **kwargs) + super().__init__( + self.greater_than, + output_labels="value_gt_limit", + **kwargs + ) self.signals.output.true = OutputSignal("true", self) self.signals.output.false = OutputSignal("false", self) @@ -50,7 +54,7 @@ def process_run_result(self, function_output): print(f"{self.inputs.value.value} <= {self.inputs.limit.value}") self.signals.output.false() - @Workflow.wrap_as.single_value_node("sqrt") + @Workflow.wrap_as.single_value_node() def numpy_sqrt(value=0): sqrt = np.sqrt(value) print(f"sqrt({value}) = {sqrt}") From c7227eba902734d71036021b9933b7be256230c2 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 14 Jul 2023 14:57:45 -0700 Subject: [PATCH 26/59] Satisfy codacy nits --- tests/unit/workflow/test_function.py | 5 +++-- tests/unit/workflow/test_output_parser.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/unit/workflow/test_function.py b/tests/unit/workflow/test_function.py index 914d4d239..d441b24f1 100644 --- a/tests/unit/workflow/test_function.py +++ b/tests/unit/workflow/test_function.py @@ -299,10 +299,11 @@ def test_instantiation(self): @unittest.skipUnless(version_info[0] == 3 and version_info[1] >= 10, "Only supported for 3.10+") class TestSingleValue(unittest.TestCase): def test_instantiation(self): - has_defaults_and_one_return = SingleValue(plus_one) + SingleValue(plus_one) with self.assertRaises(ValueError): - too_many_labels = SingleValue(plus_one, output_labels=["z", "excess_label"]) + # Too many labels + SingleValue(plus_one, output_labels=["z", "excess_label"]) def test_item_and_attribute_access(self): class Foo: diff --git a/tests/unit/workflow/test_output_parser.py b/tests/unit/workflow/test_output_parser.py index e8b5f066f..84b63b3de 100644 --- a/tests/unit/workflow/test_output_parser.py +++ b/tests/unit/workflow/test_output_parser.py @@ -22,9 +22,9 @@ def add(x, y): self.assertListEqual(ParseOutput(add).output, ["x + y"]) with self.subTest("Weird whitespace"): - def add(x, y): + def add_with_whitespace(x, y): return x + y - self.assertListEqual(ParseOutput(add).output, ["x + y"]) + self.assertListEqual(ParseOutput(add_with_whitespace).output, ["x + y"]) with self.subTest("Multiple expressions"): def add_and_subtract(x, y): From e7b67e44801fb7b46676a870467224d6be21e20f Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 12 Jul 2023 15:19:53 -0700 Subject: [PATCH 27/59] Rebasing args as input onto output labels as kwargs --- pyiron_contrib/workflow/function.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/pyiron_contrib/workflow/function.py b/pyiron_contrib/workflow/function.py index 7ff972963..d4a894b64 100644 --- a/pyiron_contrib/workflow/function.py +++ b/pyiron_contrib/workflow/function.py @@ -365,18 +365,28 @@ def __init__( ) self._verify_that_channels_requiring_update_all_exist() - self.run_on_updates = False - # Temporarily disable running on updates to set all initial values at once + self.run_on_updates = run_on_updates + self._batch_update_input(**kwargs) + + if update_on_instantiation: + self.update() + + def _batch_update_input(self, **kwargs): + """ + Temporarily disable running on updates to set all input values at once. + + Args: + **kwargs: input label - input value (including channels for connection) + pairs. + """ + run_on_updates, self.run_on_updates = self.run_on_updates, False for k, v in kwargs.items(): if k in self.inputs.labels: self.inputs[k] = v - elif k not in self._init_keywords: - warnings.warn(f"The keyword '{k}' was received but not used.") + elif k not in self._input_args.keys(): + warnings.warn(f"The keyword '{k}' was not found among input labels.") self.run_on_updates = run_on_updates # Restore provided value - if update_on_instantiation: - self.update() - def _get_output_labels(self, output_labels: str | list[str] | tuple[str] | None): """ If output labels are provided, turn convert them to a list if passed as a From 9cab601e2518872f5a38d3a69e63a90d7625c3a3 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 12 Jul 2023 15:38:20 -0700 Subject: [PATCH 28/59] Use __call__ to batch-update inputs --- pyiron_contrib/workflow/function.py | 33 ++++++++++++++++++++++++++-- tests/unit/workflow/test_function.py | 29 ++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/pyiron_contrib/workflow/function.py b/pyiron_contrib/workflow/function.py index d4a894b64..88749d635 100644 --- a/pyiron_contrib/workflow/function.py +++ b/pyiron_contrib/workflow/function.py @@ -64,6 +64,11 @@ class Function(Node): call, such that output data gets pushed after the node stops running but before then `ran` signal fires: run, process and push result, ran. + After a node is instantiated, its input can be updated as `*args` and/or `**kwargs` + on call. + This invokes an `update()` call, which can in turn invoke `run()` if + `run_on_updates` is set to `True`. + Args: node_function (callable): The function determining the behaviour of the node. label (str): The node's label. (Defaults to the node function's name.) @@ -561,8 +566,32 @@ def process_run_result(self, function_output): for out, value in zip(self.outputs, function_output): out.update(value) - def __call__(self) -> None: - self.run() + def _convert_input_args_and_kwargs_to_input_kwargs(self, *args, **kwargs): + reverse_keys = list(self._input_args.keys())[::-1] + if len(args) > len(reverse_keys): + raise ValueError( + f"Received {len(args)} positional arguments, but the node {self.label}" + f"only accepts {len(reverse_keys)} inputs." + ) + + positional_keywords = reverse_keys[-len(args):] + if len(set(positional_keywords).intersection(kwargs.keys())) > 0: + raise ValueError( + f"Cannot use {set(positional_keywords).intersection(kwargs.keys())} " + f"as both positional _and_ keyword arguments" + ) + + for arg in args: + key = positional_keywords.pop() + kwargs[key] = arg + + return kwargs + + def __call__(self, *args, **kwargs) -> None: + kwargs = self._convert_input_args_and_kwargs_to_input_kwargs(*args, **kwargs) + self._batch_update_input(**kwargs) + if self.run_on_updates: + self.run() def to_dict(self): return { diff --git a/tests/unit/workflow/test_function.py b/tests/unit/workflow/test_function.py index d441b24f1..df4cdbf53 100644 --- a/tests/unit/workflow/test_function.py +++ b/tests/unit/workflow/test_function.py @@ -271,6 +271,35 @@ def with_messed_self(x: float, self) -> float: self.assertEqual(len(warning_list), 1) + def test_call(self): + node = Function(no_default, "output", run_on_updates=False) + + with self.assertRaises(ValueError): + # More input args than there are input channels + node(1, 2, 3) + + with self.assertRaises(ValueError): + # Using input as an arg _and_ a kwarg + node(1, y=2, x=3) + + node(1, y=2) + self.assertEqual( + node.inputs.x.value, 1, msg="__call__ should accept args to update input" + ) + self.assertEqual( + node.inputs.y.value, 2, msg="__call__ should accept kwargs to update input" + ) + self.assertEqual( + node.outputs.output.value, NotData, msg="__call__ should not run things" + ) + node.run_on_updates = True + node(3) # Implicitly test partial update + self.assertEqual( + no_default(3, 2), + node.outputs.output.value, + msg="__call__ should invoke update s.t. run gets called if run_on_updates" + ) + @unittest.skipUnless(version_info[0] == 3 and version_info[1] >= 10, "Only supported for 3.10+") class TestSlow(unittest.TestCase): From b02f81481fa941f6c141c92b032cb36512f5c9fe Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 17 Jul 2023 09:44:39 -0700 Subject: [PATCH 29/59] Make output label a kwarg not arg --- tests/unit/workflow/test_function.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/workflow/test_function.py b/tests/unit/workflow/test_function.py index df4cdbf53..b4d8ec0fb 100644 --- a/tests/unit/workflow/test_function.py +++ b/tests/unit/workflow/test_function.py @@ -272,7 +272,7 @@ def with_messed_self(x: float, self) -> float: self.assertEqual(len(warning_list), 1) def test_call(self): - node = Function(no_default, "output", run_on_updates=False) + node = Function(no_default, output_labels="output", run_on_updates=False) with self.assertRaises(ValueError): # More input args than there are input channels From 3647d9056ce20cded4219f2ac033461d89870b02 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 17 Jul 2023 10:00:54 -0700 Subject: [PATCH 30/59] Split test into subtests --- tests/unit/workflow/test_function.py | 53 +++++++++++++++------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/tests/unit/workflow/test_function.py b/tests/unit/workflow/test_function.py index b4d8ec0fb..f81d63e98 100644 --- a/tests/unit/workflow/test_function.py +++ b/tests/unit/workflow/test_function.py @@ -274,31 +274,36 @@ def with_messed_self(x: float, self) -> float: def test_call(self): node = Function(no_default, output_labels="output", run_on_updates=False) - with self.assertRaises(ValueError): - # More input args than there are input channels - node(1, 2, 3) - - with self.assertRaises(ValueError): - # Using input as an arg _and_ a kwarg - node(1, y=2, x=3) + with self.subTest("Ensure desired failures occur"): + with self.assertRaises(ValueError): + # More input args than there are input channels + node(1, 2, 3) - node(1, y=2) - self.assertEqual( - node.inputs.x.value, 1, msg="__call__ should accept args to update input" - ) - self.assertEqual( - node.inputs.y.value, 2, msg="__call__ should accept kwargs to update input" - ) - self.assertEqual( - node.outputs.output.value, NotData, msg="__call__ should not run things" - ) - node.run_on_updates = True - node(3) # Implicitly test partial update - self.assertEqual( - no_default(3, 2), - node.outputs.output.value, - msg="__call__ should invoke update s.t. run gets called if run_on_updates" - ) + with self.assertRaises(ValueError): + # Using input as an arg _and_ a kwarg + node(1, y=2, x=3) + + with self.subTest("Make sure data updates work as planned"): + node(1, y=2) + self.assertEqual( + node.inputs.x.value, 1, msg="__call__ should accept args to update input" + ) + self.assertEqual( + node.inputs.y.value, 2, msg="__call__ should accept kwargs to update input" + ) + self.assertEqual( + node.outputs.output.value, NotData, msg="__call__ should not run things" + ) + node.run_on_updates = True + node(3) # Implicitly test partial update + self.assertEqual( + no_default(3, 2), + node.outputs.output.value, + msg="__call__ should invoke update s.t. run gets called if run_on_updates" + ) + + with self.subTest("Check that node kwargs can also be updated"): + pass @unittest.skipUnless(version_info[0] == 3 and version_info[1] >= 10, "Only supported for 3.10+") From 83dfabb0e95f75ead77c1faafe83fd691b8d4725 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 17 Jul 2023 10:06:05 -0700 Subject: [PATCH 31/59] Fail more generally if input not found --- pyiron_contrib/workflow/function.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyiron_contrib/workflow/function.py b/pyiron_contrib/workflow/function.py index 88749d635..649c2efbe 100644 --- a/pyiron_contrib/workflow/function.py +++ b/pyiron_contrib/workflow/function.py @@ -388,7 +388,7 @@ def _batch_update_input(self, **kwargs): for k, v in kwargs.items(): if k in self.inputs.labels: self.inputs[k] = v - elif k not in self._input_args.keys(): + else: warnings.warn(f"The keyword '{k}' was not found among input labels.") self.run_on_updates = run_on_updates # Restore provided value From 39734896994393714685602ed1605f6a526ea3b3 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 17 Jul 2023 10:07:32 -0700 Subject: [PATCH 32/59] Make error message more informative --- pyiron_contrib/workflow/function.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyiron_contrib/workflow/function.py b/pyiron_contrib/workflow/function.py index 649c2efbe..b7435f3d1 100644 --- a/pyiron_contrib/workflow/function.py +++ b/pyiron_contrib/workflow/function.py @@ -389,7 +389,12 @@ def _batch_update_input(self, **kwargs): if k in self.inputs.labels: self.inputs[k] = v else: - warnings.warn(f"The keyword '{k}' was not found among input labels.") + warnings.warn( + f"The keyword '{k}' was not found among input labels. If you are " + f"trying to update a node keyword, please use attribute assignment " + f"directly instead of calling, e.g. " + f"`my_node_instance.run_on_updates = False`." + ) self.run_on_updates = run_on_updates # Restore provided value def _get_output_labels(self, output_labels: str | list[str] | tuple[str] | None): From 8233cb02257ab749b96f2046c06fdcfe28048c0d Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 17 Jul 2023 10:09:44 -0700 Subject: [PATCH 33/59] Refactor: pull method up to parent --- pyiron_contrib/workflow/function.py | 21 --------------------- pyiron_contrib/workflow/node.py | 22 ++++++++++++++++++++++ 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/pyiron_contrib/workflow/function.py b/pyiron_contrib/workflow/function.py index b7435f3d1..598edd8e2 100644 --- a/pyiron_contrib/workflow/function.py +++ b/pyiron_contrib/workflow/function.py @@ -376,27 +376,6 @@ def __init__( if update_on_instantiation: self.update() - def _batch_update_input(self, **kwargs): - """ - Temporarily disable running on updates to set all input values at once. - - Args: - **kwargs: input label - input value (including channels for connection) - pairs. - """ - run_on_updates, self.run_on_updates = self.run_on_updates, False - for k, v in kwargs.items(): - if k in self.inputs.labels: - self.inputs[k] = v - else: - warnings.warn( - f"The keyword '{k}' was not found among input labels. If you are " - f"trying to update a node keyword, please use attribute assignment " - f"directly instead of calling, e.g. " - f"`my_node_instance.run_on_updates = False`." - ) - self.run_on_updates = run_on_updates # Restore provided value - def _get_output_labels(self, output_labels: str | list[str] | tuple[str] | None): """ If output labels are provided, turn convert them to a list if passed as a diff --git a/pyiron_contrib/workflow/node.py b/pyiron_contrib/workflow/node.py index 76e67733e..be4a0e6f3 100644 --- a/pyiron_contrib/workflow/node.py +++ b/pyiron_contrib/workflow/node.py @@ -5,6 +5,7 @@ from __future__ import annotations +import warnings from abc import ABC, abstractmethod from concurrent.futures import Future from typing import Optional, TYPE_CHECKING @@ -275,3 +276,24 @@ def fully_connected(self): and self.outputs.fully_connected and self.signals.fully_connected ) + + def _batch_update_input(self, **kwargs): + """ + Temporarily disable running on updates to set all input values at once. + + Args: + **kwargs: input label - input value (including channels for connection) + pairs. + """ + run_on_updates, self.run_on_updates = self.run_on_updates, False + for k, v in kwargs.items(): + if k in self.inputs.labels: + self.inputs[k] = v + else: + warnings.warn( + f"The keyword '{k}' was not found among input labels. If you are " + f"trying to update a node keyword, please use attribute assignment " + f"directly instead of calling, e.g. " + f"`my_node_instance.run_on_updates = False`." + ) + self.run_on_updates = run_on_updates # Restore provided value From 8b541fde5f29e4e26996228f138af6ce6fffdccd Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 17 Jul 2023 10:11:03 -0700 Subject: [PATCH 34/59] Move call up to Node --- pyiron_contrib/workflow/function.py | 4 +--- pyiron_contrib/workflow/node.py | 5 +++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pyiron_contrib/workflow/function.py b/pyiron_contrib/workflow/function.py index 598edd8e2..fc8f38ad8 100644 --- a/pyiron_contrib/workflow/function.py +++ b/pyiron_contrib/workflow/function.py @@ -573,9 +573,7 @@ def _convert_input_args_and_kwargs_to_input_kwargs(self, *args, **kwargs): def __call__(self, *args, **kwargs) -> None: kwargs = self._convert_input_args_and_kwargs_to_input_kwargs(*args, **kwargs) - self._batch_update_input(**kwargs) - if self.run_on_updates: - self.run() + return super().__call__(**kwargs) def to_dict(self): return { diff --git a/pyiron_contrib/workflow/node.py b/pyiron_contrib/workflow/node.py index be4a0e6f3..b9f901c3c 100644 --- a/pyiron_contrib/workflow/node.py +++ b/pyiron_contrib/workflow/node.py @@ -297,3 +297,8 @@ def _batch_update_input(self, **kwargs): f"`my_node_instance.run_on_updates = False`." ) self.run_on_updates = run_on_updates # Restore provided value + + def __call__(self, **kwargs) -> None: + self._batch_update_input(**kwargs) + if self.run_on_updates: + self.run() From 75e5cfedeaacc8bc5ce5a8285e0215ff41539c43 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 17 Jul 2023 10:25:58 -0700 Subject: [PATCH 35/59] Test unused kwargs showing up in the call --- tests/unit/workflow/test_function.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/unit/workflow/test_function.py b/tests/unit/workflow/test_function.py index f81d63e98..d5370b66a 100644 --- a/tests/unit/workflow/test_function.py +++ b/tests/unit/workflow/test_function.py @@ -303,7 +303,19 @@ def test_call(self): ) with self.subTest("Check that node kwargs can also be updated"): - pass + with self.assertWarns(Warning): + node(4, run_on_updates=False, y=5) + + self.assertTupleEqual( + (node.inputs.x.value, node.inputs.y.value), + (4, 5), + msg="The warning should not prevent other data from being parsed" + ) + + with self.assertWarns(Warning): + # It's also fine if you just have a typo in your kwarg or whatever, + # there should just be a warning that the data didn't get updated + node(some_randome_kwaaaaarg="foo") @unittest.skipUnless(version_info[0] == 3 and version_info[1] >= 10, "Only supported for 3.10+") From 24eced0cafdfbd3987b0ec2320f0cd725ee106a3 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 17 Jul 2023 10:28:32 -0700 Subject: [PATCH 36/59] Use update directly Instead of (incorrectly!) reproducing it --- pyiron_contrib/workflow/node.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyiron_contrib/workflow/node.py b/pyiron_contrib/workflow/node.py index b9f901c3c..0e46dbce7 100644 --- a/pyiron_contrib/workflow/node.py +++ b/pyiron_contrib/workflow/node.py @@ -300,5 +300,4 @@ def _batch_update_input(self, **kwargs): def __call__(self, **kwargs) -> None: self._batch_update_input(**kwargs) - if self.run_on_updates: - self.run() + self.update() From 33f770cad72fa03ae68f7ae1fa2709e45f865554 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 17 Jul 2023 11:15:34 -0700 Subject: [PATCH 37/59] Update SingleValueNode tests --- tests/unit/workflow/test_function.py | 29 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/tests/unit/workflow/test_function.py b/tests/unit/workflow/test_function.py index 082c41045..b7750c33c 100644 --- a/tests/unit/workflow/test_function.py +++ b/tests/unit/workflow/test_function.py @@ -349,11 +349,21 @@ def returns_foo() -> Foo: ) def test_repr(self): - svn = SingleValue(plus_one) - self.assertEqual( - svn.__repr__(), svn.outputs.y.value.__repr__(), - msg="SingleValueNodes should have their output as their representation" - ) + with self.subTest("Filled data"): + svn = SingleValue(plus_one) + self.assertEqual( + svn.__repr__(), svn.outputs.y.value.__repr__(), + msg="SingleValueNodes should have their output as their representation" + ) + + with self.subTest("Not data"): + svn = SingleValue(no_default, output_labels="output") + self.assertIs(svn.outputs.output.value, NotData) + self.assertTrue( + svn.__repr__().endswith(NotData.__name__), + msg="When the output is still not data, the representation should " + "indicate this" + ) def test_str(self): svn = SingleValue(plus_one) @@ -364,15 +374,6 @@ def test_str(self): "actually still a Function and not just the value you're seeing.)" ) - def test_repr(self): - svn = SingleValue(no_default, "output") - self.assertIs(svn.outputs.output.value, NotData) - self.assertTrue( - svn.__repr__().endswith(NotData.__name__), - msg="When the output is still not data, the representation should indicate " - "this" - ) - def test_easy_output_connection(self): svn = SingleValue(plus_one) regular = Function(plus_one) From 8c4a2d43398f7949f71cf72aadabcb000c0dfff5 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 17 Jul 2023 12:51:06 -0700 Subject: [PATCH 38/59] Also parse args at input --- pyiron_contrib/workflow/function.py | 7 ++++++- tests/unit/workflow/test_function.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/pyiron_contrib/workflow/function.py b/pyiron_contrib/workflow/function.py index fc8f38ad8..607a6a74c 100644 --- a/pyiron_contrib/workflow/function.py +++ b/pyiron_contrib/workflow/function.py @@ -340,6 +340,7 @@ class Function(Node): def __init__( self, node_function: callable, + *args, label: Optional[str] = None, run_on_updates: bool = True, update_on_instantiation: bool = True, @@ -371,7 +372,7 @@ def __init__( self._verify_that_channels_requiring_update_all_exist() self.run_on_updates = run_on_updates - self._batch_update_input(**kwargs) + self._batch_update_input(*args, **kwargs) if update_on_instantiation: self.update() @@ -571,6 +572,10 @@ def _convert_input_args_and_kwargs_to_input_kwargs(self, *args, **kwargs): return kwargs + def _batch_update_input(self, *args, **kwargs): + kwargs = self._convert_input_args_and_kwargs_to_input_kwargs(*args, **kwargs) + return super()._batch_update_input(**kwargs) + def __call__(self, *args, **kwargs) -> None: kwargs = self._convert_input_args_and_kwargs_to_input_kwargs(*args, **kwargs) return super().__call__(**kwargs) diff --git a/tests/unit/workflow/test_function.py b/tests/unit/workflow/test_function.py index 8545501f7..5cfffe8b3 100644 --- a/tests/unit/workflow/test_function.py +++ b/tests/unit/workflow/test_function.py @@ -45,6 +45,23 @@ def test_instantiation(self): void_node = Function(void) self.assertEqual(len(void_node.outputs), 0) + with self.subTest("Args and kwargs at initialization"): + node = Function(returns_multiple, 1, y=2) + self.assertEqual( + node.inputs.x.value, + 1, + msg="Should be able to set function input as args" + ) + self.assertEqual( + node.inputs.y.value, + 2, + msg="Should be able to set function input as kwargs" + ) + + with self.assertRaises(ValueError): + # Can't pass more args than the function takes + Function(returns_multiple, 1, 2, 3) + def test_defaults(self): with_defaults = Function(plus_one) self.assertEqual( From a591b1eadd242802cf49c97d387af1473b24b26f Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 17 Jul 2023 13:15:40 -0700 Subject: [PATCH 39/59] :bug: Allow args in children of Function as well --- pyiron_contrib/workflow/function.py | 8 ++++++-- tests/unit/workflow/test_function.py | 22 +++++++++++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/pyiron_contrib/workflow/function.py b/pyiron_contrib/workflow/function.py index 607a6a74c..2edd996d8 100644 --- a/pyiron_contrib/workflow/function.py +++ b/pyiron_contrib/workflow/function.py @@ -559,11 +559,11 @@ def _convert_input_args_and_kwargs_to_input_kwargs(self, *args, **kwargs): f"only accepts {len(reverse_keys)} inputs." ) - positional_keywords = reverse_keys[-len(args):] + positional_keywords = reverse_keys[-len(args):] if len(args) > 0 else [] # -0: if len(set(positional_keywords).intersection(kwargs.keys())) > 0: raise ValueError( f"Cannot use {set(positional_keywords).intersection(kwargs.keys())} " - f"as both positional _and_ keyword arguments" + f"as both positional _and_ keyword arguments; args {args}, kwargs {kwargs}, reverse_keys {reverse_keys}, positional_keyworkds {positional_keywords}" ) for arg in args: @@ -603,6 +603,7 @@ class Slow(Function): def __init__( self, node_function: callable, + *args, label: Optional[str] = None, run_on_updates=False, update_on_instantiation=False, @@ -612,6 +613,7 @@ def __init__( ): super().__init__( node_function, + *args, label=label, run_on_updates=run_on_updates, update_on_instantiation=update_on_instantiation, @@ -634,6 +636,7 @@ class SingleValue(Function, HasChannel): def __init__( self, node_function: callable, + *args, label: Optional[str] = None, run_on_updates=True, update_on_instantiation=True, @@ -643,6 +646,7 @@ def __init__( ): super().__init__( node_function, + *args, label=label, run_on_updates=run_on_updates, update_on_instantiation=update_on_instantiation, diff --git a/tests/unit/workflow/test_function.py b/tests/unit/workflow/test_function.py index 5cfffe8b3..70aa9f2d6 100644 --- a/tests/unit/workflow/test_function.py +++ b/tests/unit/workflow/test_function.py @@ -358,11 +358,31 @@ def test_instantiation(self): f"{slow.outputs.y.value}" ) + node = Slow(no_default, 1, y=2, output_labels="output") + node.run() + self.assertEqual( + no_default(1, 2), + node.outputs.output.value, + msg="Slow nodes should allow input initialization by arg and kwarg" + ) + node(2, y=3) + node.run() + self.assertEqual( + no_default(2, 3), + node.outputs.output.value, + msg="Slow nodes should allow input update on call by arg and kwarg" + ) + @unittest.skipUnless(version_info[0] == 3 and version_info[1] >= 10, "Only supported for 3.10+") class TestSingleValue(unittest.TestCase): def test_instantiation(self): - SingleValue(plus_one) + node = SingleValue(no_default, 1, y=2, output_labels="output") + self.assertEqual( + no_default(1, 2), + node.outputs.output.value, + msg="Single value node should allow function input by arg and kwarg" + ) with self.assertRaises(ValueError): # Too many labels From 846c177b160e68f5c67d98f2f8e635e3d9a9ca63 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 17 Jul 2023 13:16:35 -0700 Subject: [PATCH 40/59] :bug: remove output label specification --- tests/unit/workflow/test_function.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/workflow/test_function.py b/tests/unit/workflow/test_function.py index 70aa9f2d6..d0251eae4 100644 --- a/tests/unit/workflow/test_function.py +++ b/tests/unit/workflow/test_function.py @@ -154,10 +154,10 @@ def test_instantiation_update(self): ) def test_input_kwargs(self): - node = Function(plus_one, "y", x=2) + node = Function(plus_one, x=2) self.assertEqual(3, node.outputs.y.value, msg="Initialize from value") - node2 = Function(plus_one, "y", x=node.outputs.y) + node2 = Function(plus_one, x=node.outputs.y) node.update() self.assertEqual(4, node2.outputs.y.value, msg="Initialize from connection") From c86acd958134dc84a31c4a4c9f24de31633247f0 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 17 Jul 2023 13:20:04 -0700 Subject: [PATCH 41/59] Test calling a workflow --- tests/unit/workflow/test_workflow.py | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/unit/workflow/test_workflow.py b/tests/unit/workflow/test_workflow.py index b9dd2fd4a..b0bf3751c 100644 --- a/tests/unit/workflow/test_workflow.py +++ b/tests/unit/workflow/test_workflow.py @@ -189,6 +189,36 @@ def sum(a, b): "callback, and downstream nodes should proceed" ) + def test_call(self): + wf = Workflow("wf") + + wf.a = wf.add.SingleValue(fnc) + wf.b = wf.add.SingleValue(fnc) + + @Workflow.wrap_as.single_value_node(output_labels="sum") + def sum_(a, b): + return a + b + + wf.sum = sum_(wf.a, wf.b) + self.assertEqual( + wf.a.outputs.y.value + wf.b.outputs.y.value, + wf.sum.outputs.sum.value, + msg="Sanity check" + ) + wf(a_x=42, b_x=42) + self.assertEqual( + fnc(42) + fnc(42), + wf.sum.outputs.sum.value, + msg="Workflow should accept input channel kwargs and update inputs " + "accordingly" + # Since the nodes run automatically, there is no need for wf.run() here + ) + + with self.assertRaises(TypeError): + # IO is not ordered, so args make no sense for a workflow call + # We _must_ use kwargs + wf(42, 42) + if __name__ == '__main__': unittest.main() From 4f9052182a43b8f62a6878b0fc81c3b1edaa0937 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 17 Jul 2023 13:29:48 -0700 Subject: [PATCH 42/59] Add __call__ stuff to docstrings --- pyiron_contrib/workflow/function.py | 8 ++++++++ pyiron_contrib/workflow/workflow.py | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/pyiron_contrib/workflow/function.py b/pyiron_contrib/workflow/function.py index 2edd996d8..883bae4bf 100644 --- a/pyiron_contrib/workflow/function.py +++ b/pyiron_contrib/workflow/function.py @@ -21,6 +21,8 @@ class Function(Node): Function nodes wrap an arbitrary python function. Node IO, including type hints, is generated automatically from the provided function. + Input data for the wrapped function can be provided as any valid combination of + `*arg` and `**kwarg` at both initialization and on calling the node. On running, the function node executes this wrapped function with its current input and uses the results to populate the node output. @@ -160,6 +162,12 @@ class Function(Node): >>> plus_minus_1.outputs.to_value_dict() {'p1': 2, 'm1': 1} + Input data can be provided to both initialization and on call as ordered args + or keyword kwargs, e.g.: + >>> plus_minus_1(2, y=3) + >>> plus_minus_1.outputs.to_value_dict() + {'p1': 3, 'm1': 2} + Finally, we might stop these updates from happening automatically, even when all the input data is present and available: >>> plus_minus_1 = Function( diff --git a/pyiron_contrib/workflow/workflow.py b/pyiron_contrib/workflow/workflow.py index 8c9f7936d..73d18f648 100644 --- a/pyiron_contrib/workflow/workflow.py +++ b/pyiron_contrib/workflow/workflow.py @@ -85,6 +85,13 @@ class Workflow(Composite): >>> print(wf.outputs.second_y.value) 2 + 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 + 12 + Workflows also give access to packages of pre-built nodes under different namespaces, e.g. >>> wf = Workflow("with_prebuilt") From 4fa1d30e4b5916670a85adf7ab82a13237b79355 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 17 Jul 2023 13:47:04 -0700 Subject: [PATCH 43/59] Update the example notebook --- notebooks/workflow_example.ipynb | 315 +++++++++++++++++++++---------- 1 file changed, 220 insertions(+), 95 deletions(-) diff --git a/notebooks/workflow_example.ipynb b/notebooks/workflow_example.ipynb index 1ef82d6f8..9ed57c66e 100644 --- a/notebooks/workflow_example.ipynb +++ b/notebooks/workflow_example.ipynb @@ -4,12 +4,14 @@ "cell_type": "code", "execution_count": 1, "id": "8dee8129-6b23-4abf-90d2-217d71b8ba7a", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "e23eaad8312941fbbaa0683e71bc8ed6", + "model_id": "88c66e527673496c8f5b7ea75538baa0", "version_major": 2, "version_minor": 0 }, @@ -325,6 +327,36 @@ "adder_node.outputs.sum_.value" ] }, + { + "cell_type": "markdown", + "id": "416ba898-21ee-4638-820f-0f04a98a6706", + "metadata": {}, + "source": [ + "We can also set new input as any valid combination of kwargs and/or args at both instantiation or on call:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "0c8f09a7-67c4-4c6c-a021-e3fea1a16576", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "30" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "adder_node(10, y=20)\n", + "adder_node.outputs.sum_.value" + ] + }, { "cell_type": "markdown", "id": "07a22cee-e340-4551-bb81-07d8be1d152b", @@ -341,7 +373,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "id": "61b43a9b-8dad-48b7-9194-2045e465793b", "metadata": {}, "outputs": [], @@ -351,7 +383,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 14, "id": "647360a9-c971-4272-995c-aa01e5f5bb83", "metadata": {}, "outputs": [ @@ -389,7 +421,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 15, "id": "8fb0671b-045a-4d71-9d35-f0beadc9cf3a", "metadata": {}, "outputs": [ @@ -399,13 +431,13 @@ "-10" ] }, - "execution_count": 14, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "subtract_node(x=10, y=20).outputs.diff.value" + "subtract_node(10, 20).outputs.diff.value" ] }, { @@ -420,7 +452,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 16, "id": "5ce91f42-7aec-492c-94fb-2320c971cd79", "metadata": {}, "outputs": [ @@ -439,7 +471,7 @@ " return sum_\n", "\n", "add1 = add_node()\n", - "add2 = add_node(x=2, y=2)\n", + "add2 = add_node(2, 2)\n", "sub = subtract_node(x=add1.outputs.sum_, y=add2.outputs.sum_)\n", "print(\n", " f\"{add1.outputs.sum_.value} - {add2.outputs.sum_.value} = {sub.outputs.diff.value}\"\n", @@ -456,7 +488,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 17, "id": "20360fe7-b422-4d78-9bd1-de233f28c8df", "metadata": {}, "outputs": [ @@ -489,7 +521,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 18, "id": "1a4e9693-0980-4435-aecc-3331d8b608dd", "metadata": {}, "outputs": [], @@ -501,7 +533,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 19, "id": "7c4d314b-33bb-4a67-bfb9-ed77fba3949c", "metadata": {}, "outputs": [ @@ -530,77 +562,6 @@ "print(lin.mean()) # Finds the method on the output -- a special feature of SingleValueNode" ] }, - { - "cell_type": "markdown", - "id": "a1a9daa5-9c12-4c2f-b8bd-a54a5fc60feb", - "metadata": {}, - "source": [ - "# Workflows\n", - "\n", - "Typically, you will have a group of nodes working together with their connections.\n", - "We call these groups workflows, and offer a `Workflow(Node)` object as a single point of entry -- 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 can also rename our node output channels using the `output_labels: Optional[str | list[str] | tuple[str]` kwarg, which we'll see here" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "1cd000bd-9b24-4c39-9cac-70a3291d0660", - "metadata": {}, - "outputs": [], - "source": [ - "from pyiron_contrib.workflow.workflow import Workflow\n", - "\n", - "@Workflow.wrap_as.single_value_node(output_labels=\"is_greater\")\n", - "def greater_than_half(x: int | float | bool = 0) -> bool:\n", - " \"\"\"The functionality doesn't matter here, it's just an example\"\"\"\n", - " return x > 0.5" - ] - }, - { - "cell_type": "markdown", - "id": "ceef526f-3583-4d87-a69d-1ac3d2e706d2", - "metadata": {}, - "source": [ - "## Adding nodes to a workflow\n", - "\n", - "All five of the following approaches are equivalent ways to add a node to a workflow:" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "7964df3c-55af-4c25-afc5-9e07accb606a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "n1 n1 n1 (GreaterThanHalf) output single-value: False\n", - "n3 n3 n3 (GreaterThanHalf) output single-value: False\n", - "n4 n4 n4 (GreaterThanHalf) output single-value: False\n", - "n5 n5 n5 (GreaterThanHalf) output single-value: False\n" - ] - } - ], - "source": [ - "from pyiron_contrib.workflow.function import Slow\n", - "\n", - "n1 = greater_than_half(label=\"n1\")\n", - "\n", - "wf = Workflow(\"my_wf\", n1) # As args at init\n", - "wf.add.Slow(lambda: x + 1, output_labels=\"p1\", label=\"n2\") # Instantiating from the class with a lambda function\n", - "# (Slow since we don't have an x default)\n", - "wf.add(greater_than_half(label=\"n3\")) # Instantiating then passing to node adder\n", - "wf.n4 = greater_than_half(label=\"will_get_overwritten_with_n4\") # Set attribute to instance\n", - "greater_than_half(label=\"n5\", parent=wf) # By passing the workflow to the node\n", - "\n", - "for k, v in wf.nodes.items():\n", - " print(k, v.label, v)" - ] - }, { "cell_type": "markdown", "id": "9b9220b0-833d-4c6a-9929-5dfa60a47d14", @@ -619,7 +580,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 20, "id": "2e418abf-7059-4e1e-9b9f-b3dc0a4b5e35", "metadata": {}, "outputs": [ @@ -660,7 +621,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 21, "id": "3310eac4-04f6-421b-9824-19bb2d680be6", "metadata": {}, "outputs": [ @@ -702,7 +663,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 22, "id": "7a6f2bce-6b5e-4321-9457-0a6790d2202a", "metadata": {}, "outputs": [], @@ -712,13 +673,13 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 23, "id": "6569014a-815b-46dd-8b47-4e1cd4584b3b", "metadata": {}, "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAiMAAAGdCAYAAADAAnMpAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAjkUlEQVR4nO3de3BU9d3H8c8mMdmgydqAJAuJGFGRmHohTDBBHscLEbS0dOoYaxGh4DSoVaTagaE1hnEm4/1KUq1ER0GkXqgwRTQztcilSgnQMUbFSjRBNmaAsoligiS/5w+e5HFNojmb3f1ls+/XzP6Rw9nk+2vU8+45ew4uY4wRAACAJXG2BwAAALGNGAEAAFYRIwAAwCpiBAAAWEWMAAAAq4gRAABgFTECAACsIkYAAIBVCbYH6I/Ozk7t379fKSkpcrlctscBAAD9YIxRa2urRo0apbi4vs9/REWM7N+/X1lZWbbHAAAAQWhsbFRmZmaff+44Rt5++23df//9qqmpkc/n09q1azVz5szvfc+mTZu0aNEivf/++xo1apR+//vfq6SkpN8/MyUlRdLxxaSmpjodGQAAWNDS0qKsrKzu43hfHMfIV199pfPOO09z587VL37xix/cv76+XldeeaVuvPFGrVy5Ulu3btVNN92kU045pV/vl9R9aSY1NZUYAQAgyvzQRywcx8j06dM1ffr0fu//pz/9SaeeeqoeeeQRSdL48eO1Y8cOPfDAA/2OEQAAMHSF/W6af/7znyoqKgrYdsUVV2jHjh365ptven1Pe3u7WlpaAl4AAGBoCnuMNDU1KT09PWBbenq6jh07pgMHDvT6nvLycnk8nu4XH14FAGDoishzRr57rcgY0+v2LkuWLJHf7+9+NTY2hn1GAABgR9hv7c3IyFBTU1PAtubmZiUkJGj48OG9vicpKUlJSUnhHg0AAAwCYT8zUlBQoOrq6oBtb775piZOnKgTTjgh3D8eAAAMco5j5Msvv9Tu3bu1e/duScdv3d29e7caGhokHb/EMnv27O79S0pK9Nlnn2nRokX64IMPVFVVpRUrVuiOO+4IzQoAAEBUc3yZZseOHbrkkku6v160aJEk6YYbbtCzzz4rn8/XHSaSlJ2drQ0bNuj222/X8uXLNWrUKD322GPc1gsAACRJLtP1adJBrKWlRR6PR36/n4eeAYAlHZ1G2+sPqbm1TSNT3MrPTlN8HH9fGPrW3+N3VPzdNAAwmA2Wg3Q459hY61PZ+jr5/G3d27wet0pn5GharjckPwOxixgBgAEYLAfpcM6xsdanBSt36run0Zv8bVqwcqcqZ00gSDAgEXnOCAAMRV0H6W8HgPT/B+mNtb6on6Oj06hsfV2PEJHUva1sfZ06Ogf9FX8MYsQIAARhsBykwz3H9vpDPSLnuz/D52/T9vpDQX1/QCJGACAog+UgHe45mlv7/t7B7Af0hhgBgCAMloN0uOcYmeIO6X5Ab4gRAAjCYDlIh3uO/Ow0eT1u9XVPjkvHPyibn50W1PcHJGIEAIIyWA7S4Z4jPs6l0hk53d/ru99bkkpn5PC8EQwIMQIAQRgsB+lIzDEt16vKWROU4Qk8u5LhcXNbL0KCJ7ACwADEwnNGugyWh7shevT3+E2MAMAADZaD9GCZA+jC4+ABIELi41wqGDvc9hiDZg7AKT4zAgAArCJGAACAVcQIAACwihgBAABWESMAAMAqYgQAAFhFjAAAAKuIEQAAYBUxAgAArCJGAACAVcQIAACwihgBAABWESMAAMAq/tZeAABiVEen0fb6Q2pubdPIFLfys9MUH+eK+BzECAAAMWhjrU9l6+vk87d1b/N63CqdkaNpud6IzsJlGgAAYszGWp8WrNwZECKS1ORv04KVO7Wx1hfReYgRAABiSEenUdn6Ople/qxrW9n6OnV09rZHeBAjAADEkO31h3qcEfk2I8nnb9P2+kMRm4kYAQAghjS39h0iwewXCsQIAAAxZGSKO6T7hQIxAgBADMnPTpPX41ZfN/C6dPyumvzstIjNRIwAABBD4uNcKp2RI0k9gqTr69IZORF93ggxAgBAjJmW61XlrAnK8AReisnwuFU5a0LEnzPCQ88AAIhB03K9mpqTwRNYAQCAPfFxLhWMHW57DC7TAAAAu4gRAABgFTECAACsIkYAAIBVxAgAALCKGAEAAFYRIwAAwCpiBAAAWEWMAAAAq4gRAABgFTECAACsIkYAAIBVxAgAALCKGAEAAFYRIwAAwCpiBAAAWEWMAAAAq4gRAABgFTECAACsIkYAAIBVxAgAALCKGAEAAFYRIwAAwCpiBAAAWEWMAAAAq4gRAABgVVAxUlFRoezsbLndbuXl5Wnz5s3fu/+qVat03nnnadiwYfJ6vZo7d64OHjwY1MAAAGBocRwja9as0cKFC7V06VLt2rVLU6ZM0fTp09XQ0NDr/lu2bNHs2bM1b948vf/++3rppZf0r3/9S/Pnzx/w8AAAIPo5jpGHHnpI8+bN0/z58zV+/Hg98sgjysrKUmVlZa/7v/POOzrttNN06623Kjs7WxdddJF+85vfaMeOHQMeHgAARD9HMXL06FHV1NSoqKgoYHtRUZG2bdvW63sKCwu1b98+bdiwQcYYffHFF3r55Zd11VVX9flz2tvb1dLSEvACAABDk6MYOXDggDo6OpSenh6wPT09XU1NTb2+p7CwUKtWrVJxcbESExOVkZGhk08+WY8//nifP6e8vFwej6f7lZWV5WRMAAAQRYL6AKvL5Qr42hjTY1uXuro63XrrrbrrrrtUU1OjjRs3qr6+XiUlJX1+/yVLlsjv93e/GhsbgxkTAABEgQQnO48YMULx8fE9zoI0Nzf3OFvSpby8XJMnT9add94pSTr33HN14oknasqUKbrnnnvk9Xp7vCcpKUlJSUlORgMAAFHK0ZmRxMRE5eXlqbq6OmB7dXW1CgsLe33PkSNHFBcX+GPi4+MlHT+jAgAAYpvjyzSLFi3S008/raqqKn3wwQe6/fbb1dDQ0H3ZZcmSJZo9e3b3/jNmzNCrr76qyspK7d27V1u3btWtt96q/Px8jRo1KnQrAQAAUcnRZRpJKi4u1sGDB7Vs2TL5fD7l5uZqw4YNGjNmjCTJ5/MFPHNkzpw5am1t1RNPPKHf/e53Ovnkk3XppZfq3nvvDd0qAABRq6PTaHv9ITW3tmlkilv52WmKj+v9c4gYmlwmCq6VtLS0yOPxyO/3KzU11fY4AEKIA1Fs21jrU9n6Ovn8bd3bvB63SmfkaFpuz88UIrr09/jt+MwIAIQKB6LYtrHWpwUrd+q7/4+4yd+mBSt3qnLWBP45iBH8RXkArOg6EH07RKT/PxBtrPVZmgyR0NFpVLa+rkeISOreVra+Th2dg/7kPUKAGAEQcRyIsL3+UI8Q/TYjyedv0/b6Q5EbCtYQIwAijgMRmlv7/v0Hsx+iGzECIOI4EGFkijuk+yG6ESMAIo4DEfKz0+T1uNXXfVMuHf8wc352WiTHgiXECICI40CE+DiXSmfkSFKPfw66vi6dkcNt3jGCGAEQcRyIIEnTcr2qnDVBGZ7AM2AZHje39cYYHnoGwBqeMwKJB98NZf09fhMjAKziQAQMXTyBFUBUiI9zqWDscNtjALCIz4wAAACriBEAAGAVMQIAAKwiRgAAgFXECAAAsIoYAQAAVhEjAADAKmIEAABYRYwAAACriBEAAGAVMQIAAKwiRgAAgFXECAAAsIoYAQAAVhEjAADAKmIEAABYRYwAAACriBEAAGAVMQIAAKwiRgAAgFXECAAAsIoYAQAAVhEjAADAKmIEAABYRYwAAACriBEAAGAVMQIAAKwiRgAAgFXECAAAsIoYAQAAVhEjAADAKmIEAABYRYwAAACriBEAAGAVMQIAAKwiRgAAgFXECAAAsIoYAQAAVhEjAADAKmIEAABYRYwAAACriBEAAGAVMQIAAKwiRgAAgFXECAAAsIoYAQAAVhEjAADAKmIEAABYRYwAAACriBEAAGAVMQIAAKwiRgAAgFVBxUhFRYWys7PldruVl5enzZs3f+/+7e3tWrp0qcaMGaOkpCSNHTtWVVVVQQ0MAACGlgSnb1izZo0WLlyoiooKTZ48WU8++aSmT5+uuro6nXrqqb2+55prrtEXX3yhFStW6IwzzlBzc7OOHTs24OEBAED0cxljjJM3TJo0SRMmTFBlZWX3tvHjx2vmzJkqLy/vsf/GjRt17bXXau/evUpLSwtqyJaWFnk8Hvn9fqWmpgb1PQAAQGT19/jt6DLN0aNHVVNTo6KiooDtRUVF2rZtW6/vWbdunSZOnKj77rtPo0eP1llnnaU77rhDX3/9dZ8/p729XS0tLQEvAAAwNDm6THPgwAF1dHQoPT09YHt6erqampp6fc/evXu1ZcsWud1urV27VgcOHNBNN92kQ4cO9fm5kfLycpWVlTkZDQAARKmgPsDqcrkCvjbG9NjWpbOzUy6XS6tWrVJ+fr6uvPJKPfTQQ3r22Wf7PDuyZMkS+f3+7ldjY2MwYwIAgCjg6MzIiBEjFB8f3+MsSHNzc4+zJV28Xq9Gjx4tj8fTvW38+PEyxmjfvn0688wze7wnKSlJSUlJTkYDAABRytGZkcTEROXl5am6ujpge3V1tQoLC3t9z+TJk7V//359+eWX3dv27NmjuLg4ZWZmBjEyAAAYShxfplm0aJGefvppVVVV6YMPPtDtt9+uhoYGlZSUSDp+iWX27Nnd+1933XUaPny45s6dq7q6Or399tu688479etf/1rJycmhWwkAAIhKjp8zUlxcrIMHD2rZsmXy+XzKzc3Vhg0bNGbMGEmSz+dTQ0ND9/4nnXSSqqur9dvf/lYTJ07U8OHDdc011+iee+4J3SoAAEDUcvycERt4zggAANEnLM8ZAQAACDXHl2kweHV0Gm2vP6Tm1jaNTHErPztN8XG933INAMBgQYwMERtrfSpbXyefv617m9fjVumMHE3L9VqcDACA78dlmiFgY61PC1buDAgRSWryt2nByp3aWOuzNBkAAD+MGIlyHZ1GZevr1NunkLu2la2vU0fnoP+cMgAgRhEjUW57/aEeZ0S+zUjy+du0vf5Q5IYCAMABYiTKNbf2HSLB7AcAQKQRI1FuZIo7pPsBABBpMXs3zVC5DTY/O01ej1tN/rZePzfikpThOb4+AAAGo5iMkaF0G2x8nEulM3K0YOVOuaSAIOlKq9IZOVEZWgCA2BBzl2mG4m2w03K9qpw1QRmewEsxGR63KmdNiLrAAgDElpg6M/JDt8G6dPw22Kk5GVF3JmFarldTczKGxKUnAEBsiakYcXIbbMHY4ZEbLETi41xROTcAILbF1GUaboMFAGDwiakY4TZYAAAGn5iKka7bYPv6FIVLx++q4TZYAAAiJ6ZipOs2WEk9goTbYAEAsCOmYkTiNlgAAAabmLqbpgu3wQIAMHjEZIxI3AYLAMBgEXOXaQAAwOBCjAAAAKuIEQAAYBUxAgAArCJGAACAVcQIAACwihgBAABWESMAAMAqYgQAAFhFjAAAAKuIEQAAYBUxAgAArCJGAACAVcQIAACwihgBAABWESMAAMAqYgQAAFhFjAAAAKuIEQAAYBUxAgAArCJGAACAVcQIAACwihgBAABWESMAAMAqYgQAAFhFjAAAAKuIEQAAYBUxAgAArCJGAACAVcQIAACwihgBAABWJdgeAACAwaKj02h7/SE1t7ZpZIpb+dlpio9z2R5ryCNGAACQtLHWp7L1dfL527q3eT1ulc7I0bRcr8XJhj4u0wAAYt7GWp8WrNwZECKS1ORv04KVO7Wx1mdpsthAjAAAYlpHp1HZ+jqZXv6sa1vZ+jp1dPa2B0KBGAEAxLTt9Yd6nBH5NiPJ52/T9vpDkRsqxhAjAICY1tzad4gEsx+cI0YAADFtZIo7pPvBOWIEABDT8rPT5PW41dcNvC4dv6smPzstkmPFFGIEABDT4uNcKp2RI0k9gqTr69IZOTxvJIyIEQBAzJuW61XlrAnK8AReisnwuFU5awLPGQkzHnoGAICOB8nUnAyewGoBMQIAwP+Jj3OpYOxw22PEnKAu01RUVCg7O1tut1t5eXnavHlzv963detWJSQk6Pzzzw/mxwIAgCHIcYysWbNGCxcu1NKlS7Vr1y5NmTJF06dPV0NDw/e+z+/3a/bs2brsssuCHhYAAAw9LmOMo+fbTpo0SRMmTFBlZWX3tvHjx2vmzJkqLy/v833XXnutzjzzTMXHx+uvf/2rdu/e3ee+7e3tam9v7/66paVFWVlZ8vv9Sk1NdTIuAACwpKWlRR6P5weP347OjBw9elQ1NTUqKioK2F5UVKRt27b1+b5nnnlGn3zyiUpLS/v1c8rLy+XxeLpfWVlZTsYEAABRxFGMHDhwQB0dHUpPTw/Ynp6erqampl7f8/HHH2vx4sVatWqVEhL693nZJUuWyO/3d78aGxudjAkAAKJIUHfTuFyBtzkZY3psk6SOjg5dd911Kisr01lnndXv75+UlKSkpKRgRgMAAFHGUYyMGDFC8fHxPc6CNDc39zhbIkmtra3asWOHdu3apVtuuUWS1NnZKWOMEhIS9Oabb+rSSy8dwPgAACDaObpMk5iYqLy8PFVXVwdsr66uVmFhYY/9U1NT9d5772n37t3dr5KSEo0bN067d+/WpEmTBjY9AACIeo4v0yxatEjXX3+9Jk6cqIKCAj311FNqaGhQSUmJpOOf9/j888/13HPPKS4uTrm5uQHvHzlypNxud4/tAAAgNjmOkeLiYh08eFDLli2Tz+dTbm6uNmzYoDFjxkiSfD7fDz5zBAAAoIvj54zY0N/7lAEAwOARlueMAAAAhBoxAgAArCJGAACAVcQIAACwihgBAABWESMAAMAqYgQAAFhFjAAAAKuIEQAAYBUxAgAArCJGAACAVcQIAACwihgBAABWESMAAMAqYgQAAFhFjAAAAKuIEQAAYBUxAgAArCJGAACAVcQIAACwihgBAABWESMAAMAqYgQAAFhFjAAAAKuIEQAAYBUxAgAArCJGAACAVcQIAACwihgBAABWESMAAMAqYgQAAFhFjAAAAKuIEQAAYBUxAgAArCJGAACAVcQIAACwihgBAABWESMAAMAqYgQAAFhFjAAAAKuIEQAAYBUxAgAArCJGAACAVcQIAACwihgBAABWESMAAMAqYgQAAFhFjAAAAKuIEQAAYBUxAgAArCJGAACAVcQIAACwihgBAABWESMAAMAqYgQAAFhFjAAAAKuIEQAAYBUxAgAArCJGAACAVcQIAACwihgBAABWESMAAMAqYgQAAFgVVIxUVFQoOztbbrdbeXl52rx5c5/7vvrqq5o6dapOOeUUpaamqqCgQG+88UbQAwMAgKHFcYysWbNGCxcu1NKlS7Vr1y5NmTJF06dPV0NDQ6/7v/3225o6dao2bNigmpoaXXLJJZoxY4Z27do14OEBAED0cxljjJM3TJo0SRMmTFBlZWX3tvHjx2vmzJkqLy/v1/c455xzVFxcrLvuuqvXP29vb1d7e3v31y0tLcrKypLf71dqaqqTcQEAgCUtLS3yeDw/ePx2dGbk6NGjqqmpUVFRUcD2oqIibdu2rV/fo7OzU62trUpLS+tzn/Lycnk8nu5XVlaWkzEBAEAUcRQjBw4cUEdHh9LT0wO2p6enq6mpqV/f48EHH9RXX32la665ps99lixZIr/f3/1qbGx0MiYAAIgiCcG8yeVyBXxtjOmxrTerV6/W3Xffrddee00jR47sc7+kpCQlJSUFMxoAAIgyjmJkxIgRio+P73EWpLm5ucfZku9as2aN5s2bp5deekmXX36580kBAMCQ5OgyTWJiovLy8lRdXR2wvbq6WoWFhX2+b/Xq1ZozZ45eeOEFXXXVVcFNCgAAhiTHl2kWLVqk66+/XhMnTlRBQYGeeuopNTQ0qKSkRNLxz3t8/vnneu655yQdD5HZs2fr0Ucf1YUXXth9ViU5OVkejyeESwEAANHIcYwUFxfr4MGDWrZsmXw+n3Jzc7VhwwaNGTNGkuTz+QKeOfLkk0/q2LFjuvnmm3XzzTd3b7/hhhv07LPPDnwFAAAgqjl+zogN/b1PGQAADB5hec4IAABAqBEjAADAKmIEAABYRYwAAACriBEAAGAVMQIAAKwiRgAAgFXECAAAsIoYAQAAVhEjAADAKmIEAABYRYwAAACriBEAAGAVMQIAAKwiRgAAgFXECAAAsIoYAQAAVhEjAADAKmIEAABYRYwAAACriBEAAGAVMQIAAKwiRgAAgFXECAAAsIoYAQAAVhEjAADAKmIEAABYRYwAAACriBEAAGAVMQIAAKwiRgAAgFXECAAAsIoYAQAAVhEjAADAKmIEAABYRYwAAACriBEAAGAVMQIAAKwiRgAAgFUJtgcAIqmj02h7/SE1t7ZpZIpb+dlpio9z2R4LAGIaMYKYsbHWp7L1dfL527q3eT1ulc7I0bRcr8XJACC2cZkGMWFjrU8LVu4MCBFJavK3acHKndpY67M0GQCAGMGQ19FpVLa+TqaXP+vaVra+Th2dve0BAAg3YgRD3vb6Qz3OiHybkeTzt2l7/aHIDQUA6EaMYMhrbu07RILZDwAQWsQIhryRKe6Q7gcACC1iBENefnaavB63+rqB16Xjd9XkZ6dFciwAwP8hRjDkxce5VDojR5J6BEnX16UzcnjeCABYQowgJkzL9apy1gRleAIvxWR43KqcNYHnjACARTz0DDFjWq5XU3MyeAIrAAwyxAhiSnycSwVjh9seAwDwLVymAQAAVhEjAADAKmIEAABYRYwAAACriBEAAGAVMQIAAKwiRgAAgFXECAAAsIoYAQAAVkXFE1iNMZKklpYWy5MAAID+6jpudx3H+xIVMdLa2ipJysrKsjwJAABwqrW1VR6Pp88/d5kfypVBoLOzU/v371dKSopcrt7/UrOWlhZlZWWpsbFRqampEZ7QHtbNumMB646ddcfimqWhu25jjFpbWzVq1CjFxfX9yZCoODMSFxenzMzMfu2bmpo6pH6R/cW6Ywvrji2xuO5YXLM0NNf9fWdEuvABVgAAYBUxAgAArBoyMZKUlKTS0lIlJSXZHiWiWDfrjgWsO3bWHYtrlmJ33V2i4gOsAABg6BoyZ0YAAEB0IkYAAIBVxAgAALCKGAEAAFYRIwAAwKqoipGKigplZ2fL7XYrLy9Pmzdv7nPfV199VVOnTtUpp5yi1NRUFRQU6I033ojgtKHjZN1btmzR5MmTNXz4cCUnJ+vss8/Www8/HMFpQ8fJur9t69atSkhI0Pnnnx/eAcPEybr/8Y9/yOVy9Xh9+OGHEZw4NJz+vtvb27V06VKNGTNGSUlJGjt2rKqqqiI0bWg4WfOcOXN6/V2fc845EZw4NJz+rletWqXzzjtPw4YNk9fr1dy5c3Xw4MEITRs6Tte9fPlyjR8/XsnJyRo3bpyee+65CE1qgYkSL774ojnhhBPMn//8Z1NXV2duu+02c+KJJ5rPPvus1/1vu+02c++995rt27ebPXv2mCVLlpgTTjjB7Ny5M8KTD4zTde/cudO88MILpra21tTX15vnn3/eDBs2zDz55JMRnnxgnK67y+HDh83pp59uioqKzHnnnReZYUPI6brfeustI8l89NFHxufzdb+OHTsW4ckHJpjf909/+lMzadIkU11dberr6827775rtm7dGsGpB8bpmg8fPhzwO25sbDRpaWmmtLQ0soMPkNN1b9682cTFxZlHH33U7N2712zevNmcc845ZubMmRGefGCcrruiosKkpKSYF1980XzyySdm9erV5qSTTjLr1q2L8OSRETUxkp+fb0pKSgK2nX322Wbx4sX9/h45OTmmrKws1KOFVSjW/fOf/9zMmjUr1KOFVbDrLi4uNn/4wx9MaWlpVMaI03V3xch///vfCEwXPk7X/frrrxuPx2MOHjwYifHCYqD/bq9du9a4XC7z6aefhmO8sHG67vvvv9+cfvrpAdsee+wxk5mZGbYZw8HpugsKCswdd9wRsO22224zkydPDtuMNkXFZZqjR4+qpqZGRUVFAduLioq0bdu2fn2Pzs5Otba2Ki0tLRwjhkUo1r1r1y5t27ZNF198cThGDItg1/3MM8/ok08+UWlpabhHDIuB/L4vuOACeb1eXXbZZXrrrbfCOWbIBbPudevWaeLEibrvvvs0evRonXXWWbrjjjv09ddfR2LkAQvFv9srVqzQ5ZdfrjFjxoRjxLAIZt2FhYXat2+fNmzYIGOMvvjiC7388su66qqrIjFySASz7vb2drnd7oBtycnJ2r59u7755puwzWpLVMTIgQMH1NHRofT09IDt6enpampq6tf3ePDBB/XVV1/pmmuuCceIYTGQdWdmZiopKUkTJ07UzTffrPnz54dz1JAKZt0ff/yxFi9erFWrVikhISr+Muoeglm31+vVU089pVdeeUWvvvqqxo0bp8suu0xvv/12JEYOiWDWvXfvXm3ZskW1tbVau3atHnnkEb388su6+eabIzHygA30v2k+n0+vv/56VP17LQW37sLCQq1atUrFxcVKTExURkaGTj75ZD3++OORGDkkgln3FVdcoaefflo1NTUyxmjHjh2qqqrSN998owMHDkRi7IiKqv9qu1yugK+NMT229Wb16tW6++679dprr2nkyJHhGi9sgln35s2b9eWXX+qdd97R4sWLdcYZZ+iXv/xlOMcMuf6uu6OjQ9ddd53Kysp01llnRWq8sHHy+x43bpzGjRvX/XVBQYEaGxv1wAMP6H/+53/COmeoOVl3Z2enXC6XVq1a1f3Xkz/00EO6+uqrtXz5ciUnJ4d93lAI9r9pzz77rE4++WTNnDkzTJOFl5N119XV6dZbb9Vdd92lK664Qj6fT3feeadKSkq0YsWKSIwbMk7W/cc//lFNTU268MILZYxRenq65syZo/vuu0/x8fGRGDeiouLMyIgRIxQfH9+jIJubm3uU5netWbNG8+bN01/+8hddfvnl4Rwz5Aay7uzsbP34xz/WjTfeqNtvv1133313GCcNLafrbm1t1Y4dO3TLLbcoISFBCQkJWrZsmf79738rISFBf//73yM1+oAM5Pf9bRdeeKE+/vjjUI8XNsGs2+v1avTo0d0hIknjx4+XMUb79u0L67yhMJDftTFGVVVVuv7665WYmBjOMUMumHWXl5dr8uTJuvPOO3XuuefqiiuuUEVFhaqqquTz+SIx9oAFs+7k5GRVVVXpyJEj+vTTT9XQ0KDTTjtNKSkpGjFiRCTGjqioiJHExETl5eWpuro6YHt1dbUKCwv7fN/q1as1Z84cvfDCC1F1fbFLsOv+LmOM2tvbQz1e2Dhdd2pqqt577z3t3r27+1VSUqJx48Zp9+7dmjRpUqRGH5BQ/b537dolr9cb6vHCJph1T548Wfv379eXX37ZvW3Pnj2Ki4tTZmZmWOcNhYH8rjdt2qT//Oc/mjdvXjhHDItg1n3kyBHFxQUeqrrODJgo+XteB/L7PuGEE5SZman4+Hi9+OKL+slPftLjf48hwcKHZoPSdVvUihUrTF1dnVm4cKE58cQTuz9JvnjxYnP99dd37//CCy+YhIQEs3z58oDb4Q4fPmxrCUFxuu4nnnjCrFu3zuzZs8fs2bPHVFVVmdTUVLN06VJbSwiK03V/V7TeTeN03Q8//LBZu3at2bNnj6mtrTWLFy82kswrr7xiawlBcbru1tZWk5mZaa6++mrz/vvvm02bNpkzzzzTzJ8/39YSHAv2n/FZs2aZSZMmRXrckHG67meeecYkJCSYiooK88knn5gtW7aYiRMnmvz8fFtLCIrTdX/00Ufm+eefN3v27DHvvvuuKS4uNmlpaaa+vt7SCsIramLEGGOWL19uxowZYxITE82ECRPMpk2buv/shhtuMBdffHH31xdffLGR1ON1ww03RH7wAXKy7scee8ycc845ZtiwYSY1NdVccMEFpqKiwnR0dFiYfGCcrPu7ojVGjHG27nvvvdeMHTvWuN1u86Mf/chcdNFF5m9/+5uFqQfO6e/7gw8+MJdffrlJTk42mZmZZtGiRebIkSMRnnpgnK758OHDJjk52Tz11FMRnjS0nK77scceMzk5OSY5Odl4vV7zq1/9yuzbty/CUw+ck3XX1dWZ888/3yQnJ5vU1FTzs5/9zHz44YcWpo4MlzFRcp4LAAAMSUPwwhMAAIgmxAgAALCKGAEAAFYRIwAAwCpiBAAAWEWMAAAAq4gRAABgFTECAACsIkYAAIBVxAgAALCKGAEAAFb9LzGeNuwnas6eAAAAAElFTkSuQmCC", + "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/stsvOnTu1adOmsfU1NTV688031dTUpGPHjungwYPaunWrysvLtXDhwun7SQAAQFKKeQfW2tpanTp1Srt375bf71dJSYlaW1u1ZMkSSZLf74/Yc+SOO+5QMBjUH/7wB/3qV7/SxRdfrOuvv16PPfbY9P0UAAAgaTlMEtwrGR4elsfjUSAQUE5Oju1xAADAFEz19ZsPygMAAFYRIwAAwCpiBAAAWEWMAAAAq4gRAABgFTECAACsIkYAAIBVxAgAALCKGAEAAFYRIwAAwCpiBAAAWEWMAAAAq4gRAABgFTECAACsIkYAAIBVxAgAALCKGAEAAFYRIwAAwCpiBAAAWEWMAAAAq4gRAABgFTECAACsIkYAAIBVxAgAALCKGAEAAFZl2B4gWYTCRp29QxoIjigv263yoly5nA7bYwEAkPSIkSlo6/GrocUnf2Bk7JjX41Z9TbGqS7wWJwMAIPlxm+Y82nr82rLvSESISFJ/YERb9h1RW4/f0mQAAKQGYmQSobBRQ4tPZpzvjR5raPEpFB5vBQAAmApiZBKdvUNRV0S+y0jyB0bU2TuUuKEAAEgxxMgkBoITh0g86wAAQDRiZBJ52e5pXQcAAKIRI5MoL8qV1+PWRG/gdejcu2rKi3ITORYAACmFGJmEy+lQfU2xJEUFyejX9TXF7DcCAMAFIEbOo7rEq6aNpSrwRN6KKfC41bSxlH1GAAC4QGx6NgXVJV6tLS5gB1YAAGYAMTJFLqdDFUvn2x4DAICUw20aAABgFTECAACs4jYNAABTwKe3zxxiBACA8+DT22cWt2kAAJgEn94+84gRAAAmwKe3JwYxAgDABPj09sQgRgAAmACf3p4YxAgAABPg09sTgxgBAGACfHp7YhAjAABMgE9vTwxiBACASfDp7TOPTc8wa7C7IYDZik9vn1nECGYFdjcEMNvx6e0zh9s0sI7dDQEgvREjsIrdDQEAxAisYndDAAAxAqvY3RAAQIzAKnY3BAAQI7CK3Q0BAMQIrGJ3QwAAMQLr2N0QANIbm55hVmB3QwBIX8QIZg12NwSA9MRtGgAAYBUxAgAArCJGAACAVcQIAACwihgBAABWESMAAMAqYgQAAFhFjAAAAKuIEQAAYFVcMbJnzx4VFRXJ7XarrKxMHR0dk64/ffq0du3apSVLligrK0tLly7V3r174xoYAACklpi3g29ubta2bdu0Z88erV69Ws8++6zWrVsnn8+n73//++M+Z8OGDfriiy/04osv6gc/+IEGBgZ09uzZCx4eAAAkP4cxxsTyhFWrVqm0tFRNTU1jx1asWKH169ersbExan1bW5tuueUWHTt2TLm5uXENOTw8LI/Ho0AgoJycnLj+DAAAkFhTff2O6TbNmTNn1NXVpaqqqojjVVVVOnTo0LjPeeedd7Ry5Uo9/vjjWrRokZYvX64HH3xQ33777YR/z+nTpzU8PBzxAAAAqSmm2zSDg4MKhULKz8+POJ6fn6/+/v5xn3Ps2DEdOHBAbrdbb731lgYHB3XPPfdoaGhowt8baWxsVENDQyyjAQCAJBXXL7A6HI6Ir40xUcdGhcNhORwO7d+/X+Xl5brxxhv15JNP6uWXX57w6sjOnTsVCATGHsePH49nTAAAkARiujKyYMECuVyuqKsgAwMDUVdLRnm9Xi1atEgej2fs2IoVK2SM0YkTJ7Rs2bKo52RlZSkrKyuW0QAAQJKK6cpIZmamysrK1N7eHnG8vb1dlZWV4z5n9erVOnnypL766quxY5988omcTqcWL14cx8gAACCVxHybZvv27XrhhRe0d+9effzxx3rggQfU19enuro6SedusWzatGls/a233qr58+frzjvvlM/n0wcffKCHHnpIv/jFLzR37tzp+0kAAEBSinmfkdraWp06dUq7d++W3+9XSUmJWltbtWTJEkmS3+9XX1/f2Prvfe97am9v1y9/+UutXLlS8+fP14YNG/Too49O308BAACSVsz7jNjAPiMAACSfGdlnBAAAYLoRIwAAwCpiBAAAWEWMAAAAq4gRAABgFTECAACsIkYAAIBVxAgAALCKGAEAAFYRIwAAwCpiBAAAWEWMAAAAq4gRAABgFTECAACsIkYAAIBVxAgAALAqw/YAAJAKQmGjzt4hDQRHlJftVnlRrlxOh+2xgKRAjADABWrr8auhxSd/YGTsmNfjVn1NsapLvBYnA5IDt2kA4AK09fi1Zd+RiBCRpP7AiLbsO6K2Hr+lyYDkQYwAQJxCYaOGFp/MON8bPdbQ4lMoPN4KAKOIEQCIU2fvUNQVke8ykvyBEXX2DiVuKCAJESMAEKeB4MQhEs86IF0RIwAQp7xs97SuA9IVMQIAcSovypXX49ZEb+B16Ny7asqLchM5FpB0iBEAiJPL6VB9TbEkRQXJ6Nf1NcXsNwKcBzECABegusSrpo2lKvBE3oop8LjVtLGUfUaAKWDTMwC4QNUlXq0tLmAHViBOxAgATAOX06GKpfNtjwEkJW7TAAAAq4gRAABgFTECAACsIkYAAIBVxAgAALCKGAEAAFYRIwAAwCpiBAAAWEWMAAAAq4gRAABgFTECAACsIkYAAIBVxAgAALCKGAEAAFYRIwAAwCpiBAAAWEWMAAAAq4gRAABgFTECAACsIkYAAIBVxAgAALCKGAEAAFYRIwAAwCpiBAAAWEWMAAAAq4gRAABgFTECAACsyrA9AOwKhY06e4c0EBxRXrZb5UW5cjkdtscCAKQRYiSNtfX41dDikz8wMnbM63GrvqZY1SVei5MBANIJt2nSVFuPX1v2HYkIEUnqD4xoy74jauvxW5oMAJBuiJE0FAobNbT4ZMb53uixhhafQuHxVgAAML2IkTTU2TsUdUXku4wkf2BEnb1DiRsKAJC2iJE0NBCcOETiWQcAwIUgRtJQXrZ7WtcBAHAhiJE0VF6UK6/HrYnewOvQuXfVlBflJnIsAECaIkbSkMvpUH1NsSRFBcno1/U1xew3AgBICGIkTVWXeNW0sVQFnshbMQUet5o2lrLPCAAgYdj0LI1Vl3i1triAHVgBAFYRI2nO5XSoYul822MAANIYt2kAAIBVxAgAALCKGAEAAFbFFSN79uxRUVGR3G63ysrK1NHRMaXnHTx4UBkZGbrmmmvi+WsBAEAKijlGmpubtW3bNu3atUvd3d1as2aN1q1bp76+vkmfFwgEtGnTJv3kJz+Je1gAAJB6HMaYmD6addWqVSotLVVTU9PYsRUrVmj9+vVqbGyc8Hm33HKLli1bJpfLpbfffltHjx6d8t85PDwsj8ejQCCgnJycWMYFAACWTPX1O6YrI2fOnFFXV5eqqqoijldVVenQoUMTPu+ll17SZ599pvr6+in9PadPn9bw8HDEAwAApKaYYmRwcFChUEj5+fkRx/Pz89Xf3z/ucz799FPt2LFD+/fvV0bG1LY1aWxslMfjGXsUFhbGMiYAAEgicf0Cq8MRuUOnMSbqmCSFQiHdeuutamho0PLly6f85+/cuVOBQGDscfz48XjGBAAASSCmHVgXLFggl8sVdRVkYGAg6mqJJAWDQR0+fFjd3d267777JEnhcFjGGGVkZOi9997T9ddfH/W8rKwsZWVlxTIaAABIUjFdGcnMzFRZWZna29sjjre3t6uysjJqfU5Ojj788EMdPXp07FFXV6fLL79cR48e1apVqy5segAAkPRi/mya7du367bbbtPKlStVUVGh5557Tn19faqrq5N07hbL559/rldeeUVOp1MlJSURz8/Ly5Pb7Y46DgAA0lPMMVJbW6tTp05p9+7d8vv9KikpUWtrq5YsWSJJ8vv9591zBAAAYFTM+4zYwD4jAAAknxnZZwQAAGC6ESMAAMAqYgQAAFhFjAAAAKuIEQAAYBUxAgAArCJGAACAVcQIAACwihgBAABWESMAAMAqYgQAAFhFjAAAAKuIEQAAYBUxAgAArCJGAACAVcQIAACwihgBAABWESMAAMAqYgQAAFhFjAAAAKuIEQAAYBUxAgAArCJGAACAVcQIAACwihgBAABWESMAAMAqYgQAAFhFjAAAAKuIEQAAYBUxAgAArCJGAACAVcQIAACwKsP2AACQzkJho87eIQ0ER5SX7VZ5Ua5cToftsYCEIkYAwJK2Hr8aWnzyB0bGjnk9btXXFKu6xGtxMiCxuE0DABa09fi1Zd+RiBCRpP7AiLbsO6K2Hr+lyYDEI0YAIMFCYaOGFp/MON8bPdbQ4lMoPN4KIPUQIwCQYJ29Q1FXRL7LSPIHRtTZO5S4oQCLiBEASLCB4MQhEs86INkRIwCQYHnZ7mldByQ7YgQAEqy8KFdej1sTvYHXoXPvqikvyk3kWIA1xAgAJJjL6VB9TbEkRQXJ6Nf1NcXsN4K0QYwAgAXVJV41bSxVgSfyVkyBx62mjaXsM4K0wqZnAGBJdYlXa4sL2IEVaY8YAQCLXE6HKpbOtz0GYBW3aQAAgFXECAAAsIoYAQAAVhEjAADAKn6BFQCANBUKm1nxbi5iBACANNTW41dDiy/iQxu9Hrfqa4oTvs8Nt2kAAEgzbT1+bdl3JOrTo/sDI9qy74jaevwJnYcYAQAgjYTCRg0tPplxvjd6rKHFp1B4vBUzgxgBACCNdPYORV0R+S4jyR8YUWfvUMJmIkYAAEgjA8GJQySeddOBGAEAII3kZbvPvyiGddOBGAEAII2UF+XK63FrojfwOnTuXTXlRbkJm4kYAQAgjbicDtXXFEtSVJCMfl1fU5zQ/UaIEQAA0kx1iVdNG0tV4Im8FVPgcatpY2nC9xlh0zMAANJQdYlXa4sL2IEVAADY43I6VLF0vu0xuE0DAADsIkYAAIBVxAgAALCKGAEAAFYRIwAAwCpiBAAAWEWMAAAAq4gRAABgFTECAACsIkYAAIBVccXInj17VFRUJLfbrbKyMnV0dEy49s0339TatWt1ySWXKCcnRxUVFXr33XfjHhgAAKSWmGOkublZ27Zt065du9Td3a01a9Zo3bp16uvrG3f9Bx98oLVr16q1tVVdXV368Y9/rJqaGnV3d1/w8AAAIPk5jDEmliesWrVKpaWlampqGju2YsUKrV+/Xo2NjVP6M6688krV1tbq4YcfntL64eFheTweBQIB5eTkxDIuAACwZKqv3zFdGTlz5oy6urpUVVUVcbyqqkqHDh2a0p8RDocVDAaVm5s74ZrTp09reHg44gEAAFJTTDEyODioUCik/Pz8iOP5+fnq7++f0p/xxBNP6Ouvv9aGDRsmXNPY2CiPxzP2KCwsjGVMAACQROL6BVaHwxHxtTEm6th4XnvtNT3yyCNqbm5WXl7ehOt27typQCAw9jh+/Hg8YwIAgCSQEcviBQsWyOVyRV0FGRgYiLpa8r+am5u1efNmvf7667rhhhsmXZuVlaWsrKxYRgMAAEkqpisjmZmZKisrU3t7e8Tx9vZ2VVZWTvi81157TXfccYdeffVV3XTTTfFNCgAAUlJMV0Ykafv27brtttu0cuVKVVRU6LnnnlNfX5/q6uoknbvF8vnnn+uVV16RdC5ENm3apKeeekrXXnvt2FWVuXPnyuPxTOOPAgAAklHMMVJbW6tTp05p9+7d8vv9KikpUWtrq5YsWSJJ8vv9EXuOPPvsszp79qzuvfde3XvvvWPHb7/9dr388ssX/hMAAICkFvM+IzawzwgAAMlnRvYZAQAAmG7ECAAAsIoYAQAAVhEjAADAKmIEAABYRYwAAACriBEAAGAVMQIAAKwiRgAAgFXECAAAsIoYAQAAVhEjAADAKmIEAABYRYwAAACriBEAAGAVMQIAAKwiRgAAgFXECAAAsIoYAQAAVhEjAADAKmIEAABYRYwAAACriBEAAGAVMQIAAKzKsD2ALaGwUWfvkAaCI8rLdqu8KFcup8P2WAAApJ20jJG2Hr8aWnzyB0bGjnk9btXXFKu6xGtxMgAA0k/a3aZp6/Fry74jESEiSf2BEW3Zd0RtPX5LkwEAkJ7SKkZCYaOGFp/MON8bPdbQ4lMoPN4KAAAwE9IqRjp7h6KuiHyXkeQPjKizdyhxQwEAkObSKkYGghOHSDzrAADAhUurGMnLdk/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==", "text/plain": [ "
" ] @@ -758,7 +719,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 24, "id": "25f0495a-e85f-43b7-8a70-a2c9cbd51ebb", "metadata": {}, "outputs": [ @@ -768,7 +729,7 @@ "(False, False)" ] }, - "execution_count": 25, + "execution_count": 24, "metadata": {}, "output_type": "execute_result" } @@ -779,7 +740,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 25, "id": "449ce797-be62-4211-b483-c717a3d70583", "metadata": {}, "outputs": [ @@ -789,7 +750,7 @@ "(True, False)" ] }, - "execution_count": 26, + "execution_count": 25, "metadata": {}, "output_type": "execute_result" } @@ -801,13 +762,13 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 26, "id": "7008b0fc-3644-401c-b49f-9c40f9d89ac4", "metadata": {}, "outputs": [ { "data": { - "image/png": "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X7jXZtiXX35pZs2aZcrLy82VV14Zn2JTTCTX5pZbbjHz5s0zDQ0NprW11fzzn/80e/fujWPVqSHca7N7926TlpZmnnnmGXPs2DGze/duc/nll5vFixfHuXJnq6urM+vWrTNvvfWWkWTeeeedsx5/7Ngxc8EFF5iHHnrItLS0mJdeesmMGzfOvPnmm2G9L4HFQUpKSkxlZWVA22WXXWYeffTRkF+joKDAVFdXR7u0lBfptVmyZIn57W9/a6qqqggsMRLutfnLX/5iPB6P6erqikd5KS3ca/O73/3OzJo1K6Dt2WefNdOnT49ZjakulMDyq1/9ylx22WUBbffdd5+5+uqrw3ovhoQc4uTJkzp48KDKy8sD2svLy7Vv376QXmNwcFC9vb3Kzs6ORYkpK9Jr88orr+jo0aOqqqqKdYkpK5Jr895772nu3Ll66qmnNG3aNF1yySV6+OGH9e2338aj5JQRybUpKyvTiRMnVFdXJ2OMvvjiC7355pu6+eab41EyxrB///6g63jjjTfqwIED+u6770J+naS4WzPOrbOzUwMDA8rNzQ1oz83NVXt7e0iv8fvf/15ff/21brvttliUmLIiuTaffPKJHn30Ue3evVsZGXxMYyWSa3Ps2DHt2bNHbrdb77zzjjo7O3X//feru7ubeSxRFMm1KSsr09atW7VkyRL19fXp1KlTuuWWW/Tcc8/Fo2SMob29fdTreOrUKXV2dsrr9Yb0OvSwOIzL5Qp4bowJahvNtm3b9Pjjj2v79u2aMmVKrMpLaaFem4GBAd1xxx2qrq7WJZdcEq/yUlo4n5vBwUG5XC5t3bpVJSUluummm/T000/r1VdfpZclBsK5Ni0tLVq5cqUee+wxHTx4UPX19WptbVVlZWU8SsVZjHYdR2s/G351c4jJkycrPT096DePjo6OoGR7pu3bt+uee+7RG2+8oRtuuCGWZaakcK9Nb2+vDhw4oKamJj344IOShr4kjTHKyMjQBx98oOuvvz4utTtdJJ8br9eradOmyePxjLTNmTNHxhidOHFCF198cUxrThWRXJuamhpdc801euSRRyRJV1xxhSZOnKj58+friSeeCPk3eURXXl7eqNcxIyNDOTk5Ib8OPSwOMX78eBUXF6uhoSGgvaGhQWVlZWOet23bNt111116/fXXGeeNkXCvTVZWlj788EM1NzePPCorK3XppZequblZ8+bNi1fpjhfJ5+aaa67R559/rq+++mqk7eOPP1ZaWpqmT58e03pTSSTX5ptvvlFaWuDXWnp6uqT//UaP+CstLQ26jh988IHmzp2rcePGhf5CYU3RhdWGlwBu2rTJtLS0mFWrVpmJEyea//73v8YYYx599FFz5513jhz/+uuvm4yMDLNhwwbj8/lGHl9++WWifgTHCvfanIlVQrET7rXp7e0106dPN7feeqv56KOPzM6dO83FF19s7r333kT9CI4V7rV55ZVXTEZGhtm4caM5evSo2bNnj5k7d64pKSlJ1I/gSL29vaapqck0NTUZSebpp582TU1NI8vNz7wuw8uaV69ebVpaWsymTZtY1gxjNmzYYGbOnGnGjx9vioqKzM6dO0f+bPny5ebaa68deX7ttdcaSUGP5cuXx7/wFBDOtTkTgSW2wr02R44cMTfccIOZMGGCmT59ulmzZo355ptv4lx1agj32jz77LOmoKDATJgwwXi9XvOzn/3MnDhxIs5VO9vf/va3s353jHZd/v73v5sf/vCHZvz48eaiiy4ytbW1Yb+vyxj6yQAAgN2YwwIAAKxHYAEAANYjsAAAAOsRWAAAgPUILAAAwHoEFgAAYD0CCwAAsB6BBQAAWI/AAgAArEdgAQAA1iOwAAAA6/0/dvCXVJIj9t8AAAAASUVORK5CYII=", + "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", "text/plain": [ "
" ] @@ -828,6 +789,170 @@ "Note that in the second cell, `f` is trying to update itself as soon as its inputs are ready, so if we _hadn't_ set the `f.inputs.y` channel to wait for an update, we would have gotten an error from the plotting command due to the mis-matched lengths of the x- and y-arrays." ] }, + { + "cell_type": "markdown", + "id": "5dc12164-b663-405b-872f-756996f628bd", + "metadata": {}, + "source": [ + "# Workflows\n", + "\n", + "The case where we have groups of connected nodes working together is our normal, intended use case.\n", + "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", + "This way we can always have convenient dot-based access (and tab completion) instead of having to access things by string-based keys." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "1cd000bd-9b24-4c39-9cac-70a3291d0660", + "metadata": {}, + "outputs": [], + "source": [ + "from pyiron_contrib.workflow.workflow import Workflow\n", + "\n", + "@Workflow.wrap_as.single_value_node(output_labels=\"is_greater\")\n", + "def greater_than_half(x: int | float | bool = 0) -> bool:\n", + " \"\"\"The functionality doesn't matter here, it's just an example\"\"\"\n", + " return x > 0.5" + ] + }, + { + "cell_type": "markdown", + "id": "8f17751c-f5bf-4b13-8275-0685d8a1629e", + "metadata": {}, + "source": [ + "## Adding nodes to a workflow\n", + "\n", + "All five of the following approaches are equivalent ways to add a node to a workflow:" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "7964df3c-55af-4c25-afc5-9e07accb606a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "n1 n1 n1 (GreaterThanHalf) output single-value: False\n", + "n3 n3 n3 (GreaterThanHalf) output single-value: False\n", + "n4 n4 n4 (GreaterThanHalf) output single-value: False\n", + "n5 n5 n5 (GreaterThanHalf) output single-value: False\n" + ] + } + ], + "source": [ + "from pyiron_contrib.workflow.function import Slow\n", + "\n", + "n1 = greater_than_half(label=\"n1\")\n", + "\n", + "wf = Workflow(\"my_wf\", n1) # As args at init\n", + "wf.add.Slow(lambda: x + 1, output_labels=\"p1\", label=\"n2\") # Instantiating from the class with a lambda function\n", + "# (Slow since we don't have an x default)\n", + "wf.add(greater_than_half(label=\"n3\")) # Instantiating then passing to node adder\n", + "wf.n4 = greater_than_half(label=\"will_get_overwritten_with_n4\") # Set attribute to instance\n", + "greater_than_half(label=\"n5\", parent=wf) # By passing the workflow to the node\n", + "\n", + "for k, v in wf.nodes.items():\n", + " print(k, v.label, v)" + ] + }, + { + "cell_type": "markdown", + "id": "dd5768a4-1810-4675-9389-bceb053cddfa", + "metadata": {}, + "source": [ + "Workflows have inputs and outputs just like function nodes, but these are dynamically created to map to all _unconnected_ input and output for their underlying graph:" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "809178a5-2e6b-471d-89ef-0797db47c5ad", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/huber/work/pyiron/pyiron_contrib/pyiron_contrib/workflow/io.py:80: UserWarning: Assigning a channel with the label sum_ to the io key sum_sum_\n", + " warn(\n" + ] + } + ], + "source": [ + "wf = Workflow(\"simple\")\n", + "\n", + "@Workflow.wrap_as.single_value_node()\n", + "def add_one(x):\n", + " y = x + 1\n", + " return y\n", + "\n", + "wf.a = add_one(0)\n", + "wf.b = add_one(0)\n", + "wf.sum = add_node(wf.a, wf.b) \n", + "# Remember, with single value nodes we can pass the whole node instead of an output channel!\n", + "\n", + "print(wf.outputs.sum_sum_.value)" + ] + }, + { + "cell_type": "markdown", + "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:" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "52c48d19-10a2-4c48-ae81-eceea4129a60", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "7\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/huber/work/pyiron/pyiron_contrib/pyiron_contrib/workflow/io.py:80: UserWarning: Assigning a channel with the label x to the io key a_x\n", + " warn(\n", + "/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" + ] + } + ], + "source": [ + "wf(a_x=2, b_x=3)\n", + "print(wf.outputs.sum_sum_.value)" + ] + }, + { + "cell_type": "markdown", + "id": "0d6c7e6a-d39d-4c03-9f73-d506d7975fea", + "metadata": {}, + "source": [ + "(Note, you might see warnings from the workflow IO. This is fine, it's just letting us know that its keys don't match up with the channel labels. We don't see it until we call the input because workflows generate their IO panels dynamically on request to account for the fact that connections may change.)" + ] + }, { "cell_type": "markdown", "id": "2671dc36-42a4-466b-848d-067ef7bd1d1d", @@ -844,7 +969,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 31, "id": "ae500d5e-e55b-432c-8b5f-d5892193cdf5", "metadata": {}, "outputs": [ From cd78ea7556ca014301db8979fc49c13592779850 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 18 Jul 2023 11:17:04 -0700 Subject: [PATCH 44/59] Refactor: Rename function --- tests/unit/workflow/test_workflow.py | 44 ++++++++++++++-------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/unit/workflow/test_workflow.py b/tests/unit/workflow/test_workflow.py index b0bf3751c..99e467b07 100644 --- a/tests/unit/workflow/test_workflow.py +++ b/tests/unit/workflow/test_workflow.py @@ -8,7 +8,7 @@ from pyiron_contrib.workflow.workflow import Workflow -def fnc(x=0): +def plus_one(x=0): y = x + 1 return y @@ -20,10 +20,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 +34,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 +52,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 +80,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 +95,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,7 +122,7 @@ 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) + wf.add.Function(plus_one) self.assertTrue(str(wf.fnc.working_directory.path).endswith(wf.fnc.label)) wf.working_directory.delete() @@ -192,8 +192,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 +207,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" From 5d896a5d9963f8a53c5fb8aa36c87d5565673921 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 18 Jul 2023 11:42:22 -0700 Subject: [PATCH 45/59] Make Composite conform to abstract Node spec Namely, on_run should be a property returning a callable --- pyiron_contrib/workflow/composite.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pyiron_contrib/workflow/composite.py b/pyiron_contrib/workflow/composite.py index bacc934e8..3834513bd 100644 --- a/pyiron_contrib/workflow/composite.py +++ b/pyiron_contrib/workflow/composite.py @@ -115,12 +115,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: """ From 23fea7795fcec61e4386f0e9db9ca55cfbc909cb Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 18 Jul 2023 12:02:56 -0700 Subject: [PATCH 46/59] Disallow executors for composite nodes They are not implemented and working yet, so at least fail cleanly! --- pyiron_contrib/workflow/composite.py | 11 +++++++++++ tests/unit/workflow/test_workflow.py | 5 +++++ 2 files changed, 16 insertions(+) diff --git a/pyiron_contrib/workflow/composite.py b/pyiron_contrib/workflow/composite.py index 3834513bd..623817068 100644 --- a/pyiron_contrib/workflow/composite.py +++ b/pyiron_contrib/workflow/composite.py @@ -101,6 +101,17 @@ def __init__( 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, diff --git a/tests/unit/workflow/test_workflow.py b/tests/unit/workflow/test_workflow.py index 99e467b07..668d7b6cc 100644 --- a/tests/unit/workflow/test_workflow.py +++ b/tests/unit/workflow/test_workflow.py @@ -143,6 +143,11 @@ 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): + wf.executor = "literally anything other than None should raise the error" + def test_parallel_execution(self): wf = Workflow("wf") From 9b7e23456b99bd8911a0de6ff6d7bd84c17d358d Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 18 Jul 2023 12:15:42 -0700 Subject: [PATCH 47/59] Fail cleanly with function nodes that use self too --- pyiron_contrib/workflow/function.py | 6 ++++++ tests/unit/workflow/test_function.py | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/pyiron_contrib/workflow/function.py b/pyiron_contrib/workflow/function.py index 883bae4bf..90c6844b8 100644 --- a/pyiron_contrib/workflow/function.py +++ b/pyiron_contrib/workflow/function.py @@ -535,6 +535,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/tests/unit/workflow/test_function.py b/tests/unit/workflow/test_function.py index d0251eae4..34dd0bfe8 100644 --- a/tests/unit/workflow/test_function.py +++ b/tests/unit/workflow/test_function.py @@ -3,6 +3,7 @@ from typing import Optional, Union 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 +280,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 From 2c4b30031f108554bf99340370ca4c2c829f1b53 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 18 Jul 2023 12:16:21 -0700 Subject: [PATCH 48/59] Add explanatory comment for devs --- tests/unit/workflow/test_workflow.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/workflow/test_workflow.py b/tests/unit/workflow/test_workflow.py index 668d7b6cc..af320a26f 100644 --- a/tests/unit/workflow/test_workflow.py +++ b/tests/unit/workflow/test_workflow.py @@ -146,6 +146,9 @@ def test_no_parents(self): 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): From 5b3835d52af38498be89976d22699330f11d3f88 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 18 Jul 2023 12:22:32 -0700 Subject: [PATCH 49/59] :bug: finish renaming the function used in the test suite --- tests/unit/workflow/test_workflow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/workflow/test_workflow.py b/tests/unit/workflow/test_workflow.py index af320a26f..18ea73ecd 100644 --- a/tests/unit/workflow/test_workflow.py +++ b/tests/unit/workflow/test_workflow.py @@ -123,7 +123,9 @@ def test_working_directory(self): self.assertIsInstance(wf.working_directory, DirectoryObject) self.assertTrue(str(wf.working_directory.path).endswith(wf.label)) wf.add.Function(plus_one) - self.assertTrue(str(wf.fnc.working_directory.path).endswith(wf.fnc.label)) + self.assertTrue( + str(wf.plus_one.working_directory.path).endswith(wf.plus_one.label) + ) wf.working_directory.delete() def test_no_parents(self): From 74adeeef4ab75604158ee5e0b1dcbf621488c431 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 18 Jul 2023 12:28:22 -0700 Subject: [PATCH 50/59] Return output when calling `run` And downstream stuff like `update` and thus `__call__`. This was requested by Joerg and now makes things really start to feel like regular python --- pyiron_contrib/workflow/node.py | 20 ++++---- tests/unit/workflow/test_function.py | 72 +++++++++++++++++++++++++++- tests/unit/workflow/test_workflow.py | 45 +++++++++++++++++ 3 files changed, 127 insertions(+), 10 deletions(-) diff --git a/pyiron_contrib/workflow/node.py b/pyiron_contrib/workflow/node.py index 0e46dbce7..a1da62b1f 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 @@ -154,7 +154,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 +167,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 +176,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 +195,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 +207,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 +225,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 +236,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 +302,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/tests/unit/workflow/test_function.py b/tests/unit/workflow/test_function.py index 34dd0bfe8..4d40382ad 100644 --- a/tests/unit/workflow/test_function.py +++ b/tests/unit/workflow/test_function.py @@ -1,6 +1,7 @@ -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 @@ -342,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 18ea73ecd..7a8efac73 100644 --- a/tests/unit/workflow/test_workflow.py +++ b/tests/unit/workflow/test_workflow.py @@ -5,6 +5,7 @@ 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 @@ -229,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() From 712afb90b5ab3fbf5a161996feef2f2b7daa29bc Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 18 Jul 2023 12:42:52 -0700 Subject: [PATCH 51/59] Consistently pass the Node.run_on_updates kwarg through in children --- pyiron_contrib/workflow/composite.py | 9 ++++++++- pyiron_contrib/workflow/function.py | 2 +- pyiron_contrib/workflow/workflow.py | 15 +++++++++++++-- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/pyiron_contrib/workflow/composite.py b/pyiron_contrib/workflow/composite.py index 623817068..4c6401d3d 100644 --- a/pyiron_contrib/workflow/composite.py +++ b/pyiron_contrib/workflow/composite.py @@ -92,10 +92,17 @@ def __init__( label: str, *args, parent: Optional[Composite] = None, + run_on_updates: bool = False, 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) diff --git a/pyiron_contrib/workflow/function.py b/pyiron_contrib/workflow/function.py index 90c6844b8..0aa82779a 100644 --- a/pyiron_contrib/workflow/function.py +++ b/pyiron_contrib/workflow/function.py @@ -360,6 +360,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 +380,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: diff --git a/pyiron_contrib/workflow/workflow.py b/pyiron_contrib/workflow/workflow.py index 73d18f648..4550c3e94 100644 --- a/pyiron_contrib/workflow/workflow.py +++ b/pyiron_contrib/workflow/workflow.py @@ -125,8 +125,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 = False, + 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) From 0ce3d35db325652e1db6403dafbc4ed46716bb8f Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 19 Jul 2023 09:27:51 -0700 Subject: [PATCH 52/59] Update Node docs --- pyiron_contrib/workflow/node.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pyiron_contrib/workflow/node.py b/pyiron_contrib/workflow/node.py index a1da62b1f..e5f2ec3d7 100644 --- a/pyiron_contrib/workflow/node.py +++ b/pyiron_contrib/workflow/node.py @@ -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` From 1de973f8a3b16961f44e45d052f080457655281e Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 19 Jul 2023 09:33:58 -0700 Subject: [PATCH 53/59] Update Function docs --- pyiron_contrib/workflow/function.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pyiron_contrib/workflow/function.py b/pyiron_contrib/workflow/function.py index 0aa82779a..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. From c6ee0cae60a72011dc6c5f6d9fe63f7cc1963aae Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 19 Jul 2023 09:41:25 -0700 Subject: [PATCH 54/59] Update Compositedocs --- pyiron_contrib/workflow/composite.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyiron_contrib/workflow/composite.py b/pyiron_contrib/workflow/composite.py index 4c6401d3d..c07092d24 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. From c20937b8fd474724ebc20966fbbc83a1bb72f3ec Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 19 Jul 2023 09:43:49 -0700 Subject: [PATCH 55/59] Have Composite and Workflow run on update by default We may wish to later make Macro's slow, but for Workflows, since the IO is just routing through to the owned nodes, input updates are _anyhow_ most of the time re-running things, so it's a sensible default IMO --- pyiron_contrib/workflow/composite.py | 2 +- pyiron_contrib/workflow/workflow.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyiron_contrib/workflow/composite.py b/pyiron_contrib/workflow/composite.py index c07092d24..c27b3210f 100644 --- a/pyiron_contrib/workflow/composite.py +++ b/pyiron_contrib/workflow/composite.py @@ -95,7 +95,7 @@ def __init__( label: str, *args, parent: Optional[Composite] = None, - run_on_updates: bool = False, + run_on_updates: bool = True, strict_naming: bool = True, **kwargs, ): diff --git a/pyiron_contrib/workflow/workflow.py b/pyiron_contrib/workflow/workflow.py index 4550c3e94..f03ce1ab6 100644 --- a/pyiron_contrib/workflow/workflow.py +++ b/pyiron_contrib/workflow/workflow.py @@ -129,7 +129,7 @@ def __init__( self, label: str, *nodes: Node, - run_on_updates: bool = False, + run_on_updates: bool = True, strict_naming=True ): super().__init__( From b796753a438c5a4ced07820f727db1619bcbc178 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 19 Jul 2023 09:45:40 -0700 Subject: [PATCH 56/59] Update Workflow docs --- pyiron_contrib/workflow/workflow.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pyiron_contrib/workflow/workflow.py b/pyiron_contrib/workflow/workflow.py index f03ce1ab6..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 From b674f36a71e9c8b3924c3ebce3e25e4ee5bd820f Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 19 Jul 2023 09:46:49 -0700 Subject: [PATCH 57/59] Notebook: update topic order --- notebooks/workflow_example.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebooks/workflow_example.ipynb b/notebooks/workflow_example.ipynb index 9ed57c66e..6c76b4db4 100644 --- a/notebooks/workflow_example.ipynb +++ b/notebooks/workflow_example.ipynb @@ -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 " ] }, From 52028ebc40031a24d0229f00b25f981380114a6a Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 19 Jul 2023 09:53:54 -0700 Subject: [PATCH 58/59] Notebook: show how run returns values --- notebooks/workflow_example.ipynb | 139 ++++++++++++++++++++++--------- 1 file changed, 100 insertions(+), 39 deletions(-) diff --git a/notebooks/workflow_example.ipynb b/notebooks/workflow_example.ipynb index 6c76b4db4..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 }, @@ -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" ] }, From 0e72e8941ceb90148496c4c46712bc84dc42cd34 Mon Sep 17 00:00:00 2001 From: pyiron-runner Date: Wed, 2 Aug 2023 17:34:45 +0000 Subject: [PATCH 59/59] Format black --- pyiron_contrib/workflow/composite.py | 6 +----- pyiron_contrib/workflow/function.py | 2 +- pyiron_contrib/workflow/workflow.py | 6 +----- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/pyiron_contrib/workflow/composite.py b/pyiron_contrib/workflow/composite.py index c27b3210f..77fd539a2 100644 --- a/pyiron_contrib/workflow/composite.py +++ b/pyiron_contrib/workflow/composite.py @@ -100,11 +100,7 @@ def __init__( **kwargs, ): super().__init__( - *args, - label=label, - parent=parent, - run_on_updates=run_on_updates, - **kwargs + *args, label=label, parent=parent, run_on_updates=run_on_updates, **kwargs ) self.strict_naming: bool = strict_naming self.nodes: DotDict[str:Node] = DotDict() diff --git a/pyiron_contrib/workflow/function.py b/pyiron_contrib/workflow/function.py index decc53a59..01bb04fe9 100644 --- a/pyiron_contrib/workflow/function.py +++ b/pyiron_contrib/workflow/function.py @@ -578,7 +578,7 @@ def _convert_input_args_and_kwargs_to_input_kwargs(self, *args, **kwargs): f"only accepts {len(reverse_keys)} inputs." ) - positional_keywords = reverse_keys[-len(args):] if len(args) > 0 else [] # -0: + positional_keywords = reverse_keys[-len(args) :] if len(args) > 0 else [] # -0: if len(set(positional_keywords).intersection(kwargs.keys())) > 0: raise ValueError( f"Cannot use {set(positional_keywords).intersection(kwargs.keys())} " diff --git a/pyiron_contrib/workflow/workflow.py b/pyiron_contrib/workflow/workflow.py index e2b543e80..2856090ff 100644 --- a/pyiron_contrib/workflow/workflow.py +++ b/pyiron_contrib/workflow/workflow.py @@ -131,11 +131,7 @@ class Workflow(Composite): """ def __init__( - self, - label: str, - *nodes: Node, - run_on_updates: bool = True, - strict_naming=True + self, label: str, *nodes: Node, run_on_updates: bool = True, strict_naming=True ): super().__init__( label=label,