Skip to content

Commit

Permalink
Merge pull request #200 from pyiron/solutes
Browse files Browse the repository at this point in the history
  • Loading branch information
liamhuber authored May 16, 2023
2 parents 2ae8a53 + c1b3c41 commit bcf71a6
Show file tree
Hide file tree
Showing 5 changed files with 1,861 additions and 3 deletions.
7 changes: 6 additions & 1 deletion ironflow/model/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,12 @@ def _standard_representations(self):
for i, o in enumerate(self.outputs)
if o.type_ == "data"
}
standard_reps["source code"] = display_string(self._source_code)
try:
standard_reps["source code"] = display_string(self._source_code)
except OSError:
# No source code can be found for user nodes defined in the nodebook
# But the source is available there anyhow
pass
return standard_reps

@property
Expand Down
29 changes: 27 additions & 2 deletions ironflow/nodes/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ class Select_Node(DataNode):
]
color = "#aabb44"

def update_event(self, inp=-1):
self.outputs.ports.item.dtype.valid_classes = [
type(self.inputs.values.array[self.inputs.values.i])
]
super().update_event(inp=inp)

def node_function(self, array, i, **kwargs) -> dict:
return {"item": array[i]}

Expand All @@ -47,7 +53,23 @@ class Slice_Node(DataNode):
]
color = "#aabb44"

def node_function(self, array, i, j, **kwargs) -> dict:
def update_event(self, inp=-1):
self.outputs.ports.sliced.dtype = dtypes.List(
valid_classes=list(
set(
type(obj)
for obj in self._slice(
self.inputs.values.array,
self.inputs.values.i,
self.inputs.values.j,
)
)
)
)
super().update_event(inp=inp)

@staticmethod
def _slice(array, i, j):
converted = np.array(array)
if i is None and j is None:
sliced = converted
Expand All @@ -57,7 +79,10 @@ def node_function(self, array, i, j, **kwargs) -> dict:
sliced = converted[:j]
else:
sliced = converted[i:j]
return {"sliced": sliced}
return sliced

def node_function(self, array, i, j, **kwargs) -> dict:
return {"sliced": self._slice(array, i, j)}


class Transpose_Node(DataNode):
Expand Down
17 changes: 17 additions & 0 deletions ironflow/nodes/pyiron_atomistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,23 @@ def node_function(
}


class ChangeSpecies_Node(OutputsOnlyAtoms):
title = "ChangeSpecies"
init_inputs = [
NodeInputBP(label="structure", dtype=dtypes.Data(valid_classes=Atoms)),
NodeInputBP(label="species", dtype=dtypes.String(default=None)),
NodeInputBP(
label="indices",
dtype=dtypes.List(valid_classes=[int, np.integer]),
),
]

def node_function(self, structure, species, indices, **kwargs):
structure = structure.copy()
structure[indices] = species
return {"structure": structure}


class Repeat_Node(OutputsOnlyAtoms):
"""
Repeat atomic structure supercell.
Expand Down
164 changes: 164 additions & 0 deletions notebooks/binding_energy.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "98fd7dae-4280-4060-9865-a8179bb0a571",
"metadata": {},
"source": [
"# Example: solute-grain boundary binding\n",
"\n",
"This saved workflow demonstrates how solute-grain boundary (GB) binding can be calculated in ironflow.\n",
"\n",
"In this case, we'll be examining the per-solute-atom energy cost of moving solutes from the bulk into a particular decoration pattern at the GB. I.e.\n",
"\n",
"$E_\\mathrm{bind}^{X}$ = (E_\\mathrm{GB}^X + \\eta E_\\mathrm{bulk}) - (E_\\mathrm{GB} + \\eta E_\\mathrm{bulk}^X)$\n",
"\n",
"Where $X$ denotes the presence of the solute species, bulk and GB denote the structure, and $\\eta$ is the ratio of solute atoms found in the GB structure (2 in the saved workflow below) and the bulk structure (1 in the saved workflow below)."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "03fb59eb-db0c-42a9-b315-2230abe9c334",
"metadata": {},
"outputs": [],
"source": [
"%matplotlib inline\n",
"from ironflow import GUI, node_tools as nt\n",
"from pyiron_atomistics import Atoms"
]
},
{
"cell_type": "markdown",
"id": "97468c30-e93f-450c-9916-f8a2925e7127",
"metadata": {},
"source": [
"We have most of the tools we need to do this calculation already in ironflow, but we'll need to add one final node to take our simulation results and do the math described above.\n",
"\n",
"We'll define that node here in the notebook, then register it with ironflow when we instantiate the GUI."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "55b41883-d9cb-46f0-ac57-90423ed67186",
"metadata": {},
"outputs": [],
"source": [
"class BindingEnergy_Node(nt.DataNode):\n",
" \"\"\"\n",
" The average (per-solute) energy associated with moving $n$ solutes from the bulk \n",
" into a particular decoration pattern at a grain boundary.\n",
" \"\"\"\n",
" \n",
" title = \"BindingEnergy\"\n",
" init_inputs = [\n",
" nt.NodeInputBP(label=\"bulk_en\", dtype=nt.dtypes.Float()),\n",
" nt.NodeInputBP(label=\"bulk_x_en\", dtype=nt.dtypes.Float()),\n",
" nt.NodeInputBP(label=\"gb_en\", dtype=nt.dtypes.Float()),\n",
" nt.NodeInputBP(label=\"gb_x_en\", dtype=nt.dtypes.Float()),\n",
" nt.NodeInputBP(\n",
" label=\"bulk_x_structure\",\n",
" dtype=nt.dtypes.Data(valid_classes=[Atoms])\n",
" ),\n",
" nt.NodeInputBP(\n",
" label=\"gb_x_structure\", \n",
" dtype=nt.dtypes.Data(valid_classes=[Atoms])\n",
" ),\n",
" nt.NodeInputBP(label=\"solute_species\", dtype=nt.dtypes.String(default=None)),\n",
" ]\n",
" init_outputs = [\n",
" nt.NodeOutputBP(label=\"binding_energy\", dtype=nt.dtypes.Float())\n",
" ]\n",
" \n",
" def node_function(\n",
" self, \n",
" bulk_en, \n",
" bulk_x_en, \n",
" gb_en, gb_x_en, \n",
" bulk_x_structure, \n",
" gb_x_structure, \n",
" solute_species\n",
" ):\n",
" n_solutes_in_bulk = (bulk_x_structure.get_chemical_symbols() == solute_species).sum()\n",
" n_solutes_at_gb = (gb_x_structure.get_chemical_symbols() == solute_species).sum()\n",
" bulks_per_gb = n_solutes_at_gb / n_solutes_in_bulk\n",
" \n",
" segregated_energy = gb_x_en + bulks_per_gb * bulk_en\n",
" clean_energy = gb_en + bulks_per_gb * bulk_x_en\n",
" return {\"binding_energy\": (segregated_energy - clean_energy) / n_solutes_at_gb}"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5d198f4f-584f-4666-bedf-2a815a0a68e7",
"metadata": {},
"outputs": [],
"source": [
"gui = GUI('solute_gb_binding', extra_nodes_packages=[[BindingEnergy_Node]])"
]
},
{
"cell_type": "markdown",
"id": "7daed9e3-6911-4fa3-8304-6b2b6fe41620",
"metadata": {},
"source": [
"The workflow graph is all ready to go; calculation nodes that run an underyling pyiron job are \"slow\", i.e., unlike the \"fast\" `DataNode` nodes, they need to explicitly receive a `run` command to produce output. Here we've chained them together by connecting their `ran` output signal port to the next calculations `run` input port -- so just hit `run` on the pure bulk structure and the rest will go!\n",
"\n",
"Zoom out with the negative magnifying glass button twice to see the whole workflow at once."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1a29a816-1f3a-402d-a5d1-c8219f03f0ed",
"metadata": {},
"outputs": [],
"source": [
"gui.draw()"
]
},
{
"cell_type": "markdown",
"id": "b55c4bb1-3e4d-4b41-aa56-6af358894821",
"metadata": {},
"source": [
"If you want to modify the calculation, e.g. to change the solute species, don't forget to `reset` each of the minimization nodes.\n",
"\n",
"As an exercise, you may wish to change the host species, or the GB being looked at -- perhaps changing the GB character, or maybe inserting a `Repeat` node before adding solutes to look at the dilute segregation energy. This is possible, but note that the decoration pattern is defined by the `InputArray` node used to define the `indices` for species change -- if you make any changes to the GB in question, you'll need to examine it (click `SHOW` and look at the `plot3d`) and choose new indices for your desired decoration pattern (clicking on individual atoms in the NGLView presentation of the structure shows their index, sometimes with a `^` character breaking up the number, i.e. `4^2` is `42`).\n",
"\n",
"Note that the `Lammps` node normally automatically populates its `potential` input based on its `structure`. In this case, we want to make sure that both the bulk and solute-containing calculations use the _same potential_ in order to get equivalent representations of the host species. To accomplish this, we just manually select a potential from the `LammpsPotentials` node after giving it one of the _solute containing_ structures."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ac20b798-3113-44c8-82b2-1d2cc0ee9aac",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.0"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Loading

0 comments on commit bcf71a6

Please sign in to comment.