Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Question] Creating a list from node outputs #512

Open
ligerzero-ai opened this issue Nov 27, 2024 · 4 comments
Open

[Question] Creating a list from node outputs #512

ligerzero-ai opened this issue Nov 27, 2024 · 4 comments
Labels
question Further information is requested

Comments

@ligerzero-ai
Copy link

wf.ISIF2_structures = collect_structure_from_nodejob(wf.ISIF2_job.outputs.vasp_output, energy_diff_threshold=0.2, job_name="ISIF2")
wf.ISIF5_structures = collect_structure_from_nodejob(wf.ISIF5_job.outputs.vasp_output, energy_diff_threshold=0.2, job_name="ISIF5")
wf.ISIF7_structures = collect_structure_from_nodejob(wf.ISIF7_job.outputs.vasp_output, energy_diff_threshold=0.2, job_name="ISIF7")
wf.ASSYST_base_structures = std.AppendToList(new_element=wf.ISIF2_structures.outputs.filtered_structures)
wf.ASSYST_base_structures = std.AppendToList(wf.ASSYST_base_structures, new_element=wf.ISIF5_structures.outputs.filtered_structures)
wf.ASSYST_base_structures = std.AppendToList(wf.ASSYST_base_structures, new_element=wf.ISIF7_structures.outputs.filtered_structures)

it complains:

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[5], line 53
     51 wf.ISIF7_structures = collect_structure_from_nodejob(wf.ISIF7_job.outputs.vasp_output, energy_diff_threshold=0.2, job_name="ISIF7")
     52 wf.ASSYST_base_structures = std.AppendToList(new_element=wf.ISIF2_structures.outputs.filtered_structures)
---> 53 wf.ASSYST_base_structures = std.AppendToList(wf.ASSYST_base_structures, new_element=wf.ISIF5_structures.outputs.filtered_structures)
     54 wf.ASSYST_base_structures = std.AppendToList(wf.ASSYST_base_structures, new_element=wf.ISIF7_structures.outputs.filtered_structures)
     55 wf.pull()

File /cmmc/ptmp/hmai/mambaforge/envs/pyiron_workflow/lib/python3.12/site-packages/pyiron_workflow/nodes/composite.py:404, in Composite.__setattr__(self, key, node)
    402     super().__setattr__(key, node)
    403 elif isinstance(node, Node):
--> 404     self.add_child(node, label=key)
    405 elif (
    406     isinstance(node, type)
    407     and issubclass(node, Node)
    408     and key in self.children.keys()
    409 ):
    410     # When a class is assigned to an existing node, try a replacement
    411     self.replace_child(key, node)

File /cmmc/ptmp/hmai/mambaforge/envs/pyiron_workflow/lib/python3.12/site-packages/pyiron_workflow/nodes/composite.py:286, in Composite.add_child(self, child, label, strict_naming)
    281     raise TypeError(
    282         f"Only new {Node.__name__} instances may be added, but got "
    283         f"{type(child)}."
    284     )
    285 self._cached_inputs = None  # Reset cache after graph change
--> 286 return super().add_child(child, label=label, strict_naming=strict_naming)

File /cmmc/ptmp/hmai/mambaforge/envs/pyiron_workflow/lib/python3.12/site-packages/pyiron_workflow/mixin/semantics.py:270, in SemanticParent.add_child(self, child, label, strict_naming)
    268     pass
    269 else:
--> 270     label = self._get_unique_label(label, strict_naming)
    272     if self._this_child_is_already_at_a_different_label(child, label):
    273         self.children.inv.pop(child)

File /cmmc/ptmp/hmai/mambaforge/envs/pyiron_workflow/lib/python3.12/site-packages/pyiron_workflow/mixin/semantics.py:315, in SemanticParent._get_unique_label(self, label, strict_naming)
    313 if label in self.child_labels:
    314     if strict_naming:
--> 315         raise AttributeError(
    316             f"{label} is already the label for a child. Please remove it "
    317             f"before assigning another child to this label."
    318         )
    319     else:
    320         label = self._add_suffix_to_label(label)

AttributeError: ASSYST_base_structures is already the label for a child. Please remove it before assigning another child to this label.

How am I supposed do this? I want to collate the structures in a single list.

@ligerzero-ai ligerzero-ai added the question Further information is requested label Nov 27, 2024
@ligerzero-ai ligerzero-ai changed the title Creating a list from node outputs [Question] Creating a list from node outputs Nov 27, 2024
@ligerzero-ai
Copy link
Author

FWIW, I have already tried:

wf.ASSYST_base_structures = collect_structures(node_list = [wf.ISIF2_job,
                                                             wf.ISIF5_job,
                                                             wf.ISIF7_job],
                                                energy_diff_threshold = 0.1,
                                                job_names=[f"{os.getcwd()}/{structure_folder}/ISIF2",
                                                           f"{os.getcwd()}/{structure_folder}/ISIF5",
                                                           f"{os.getcwd()}/{structure_folder}/ISIF7"])

but then it complains about trying to do something with NotData, which I assume means that the nodes have not run.

@ligerzero-ai
Copy link
Author

Using:

i2l = pwf.inputs_to_list(3)
wf.ISIF_vaspoutputs = i2l([wf.ISIF2_job.outputs.vasp_output, wf.ISIF5_job.outputs.vasp_output, wf.ISIF7_job.outputs.vasp_output])

yields

ReadinessError: InputsToList3 received a run command but is not ready. The node should be neither running nor failed, and all input values should conform to type hints.
InputsToList3 readiness: False
STATE:
running: False
failed: False
INPUTS:
item_0 ready: True
item_1 ready: False
item_2 ready: False

@liamhuber
Copy link
Member

So the exception is perfectly expected -- you can't assign new nodes to the same label. MWE:

import pyiron_workflow as pwf
wf = pwf.Workflow("label_is_taken")
wf.some_label = pwf.standard_nodes.UserInput(42)

try:
    wf.some_label = pwf.standard_nodes.UserInput(43)
except AttributeError as e:
    print("Everything is proceeding as I have foreseen --", e)
>>> Selection deleted
import pyiron_workflow as pwf
wf = pwf.Workflow("label_is_taken")
wf.some_label = pwf.standard_nodes.UserInput(42)

try:
    wf.some_label = pwf.standard_nodes.UserInput(43)
except AttributeError as e:
    print("Everything is proceeding as I have foreseen --", e)
>>> Everything is proceeding as I have foreseen -- some_label is already the label for a child. Please remove it before assigning another child to this label.

I want to collate the structures in a single list.

Good question/goal.

AppendToList can indeed be used to achieve this, but you only instantiate the node once, loop its own output back on itself, and then make multiple connections to the new_element input. This is super dependent on cyclic flows and execution signal management. Since we plan to change things so that cyclicity is only possible inside a While node and inputs only accept one connection, this is not a future-safe way of proceeding and I don't recommend it.

Do you know at the time of writing the graph how many of these structures you will have -- i.e. is it explicitly ISIF(2/5/7), or is it ISIF{N} where N is run-time input from the user? If it's fixed at the graph level, you can accomplish this super easily with an inputs-to-list node:

import pyiron_workflow as pwf

wf = pwf.Workflow("collect_fixed_set")
wf.d1 = pwf.standard_nodes.UserInput(10)
wf.d2 = pwf.standard_nodes.UserInput(20)
wf.d3 = pwf.standard_nodes.UserInput(30)
wf.as_list = pwf.inputs_to_list(  # n: int, /, *node_args, ...
    4,
    wf.d3,
    25,  # Can mix and match data and connections, as usual
    wf.d2,
    wf.d1
)
wf()
>>> {'as_list__list': [30, 25, 20, 10]}

I.e. we specify that we're collecting 4 things (under the hood this is dynamically subclassing a generic list node parent class to a subclass with fixed IO -- 4 inputs), then give input connections/data for where to get those things.

If you need to do it where the length of the list is variable at run-time, then I think we need to figure out how to express your action as a "body node" and stick it in a for loop node. If the length isn't variable the inputs-to-list node is cleaner, IMO.

I looked on pyiron_nodes for the assyst nodes but didn't see anything -- is this available somewhere I can read it? It's not necessary, but if we need to go to making a body node macro I won't be able to give any help on formulating it without access to the node package details.

@liamhuber
Copy link
Member

Using:

i2l = pwf.inputs_to_list(3)
wf.ISIF_vaspoutputs = i2l([wf.ISIF2_job.outputs.vasp_output, wf.ISIF5_job.outputs.vasp_output, wf.ISIF7_job.outputs.vasp_output])

yields

ReadinessError: InputsToList3 received a run command but is not ready. The node should be neither running nor failed, and all input values should conform to type hints.
InputsToList3 readiness: False
STATE:
running: False
failed: False
INPUTS:
item_0 ready: True
item_1 ready: False
item_2 ready: False

Yeah, the problem here is [wf.ISIF2_job.outputs.vasp_output, wf.ISIF5_job.outputs.vasp_output, wf.ISIF7_job.outputs.vasp_output]. Python doesn't know how to make a list of the output channels and so the node tries to treat the whole list as a single piece of input data instead of a bunch on input connections. You were absolutely on the right track here though, and I think my co-temporaneously written example should clear it up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants