Skip to content

Commit

Permalink
- Restructered node library with several new nodes
Browse files Browse the repository at this point in the history
- more generic classes for calc_md, calc_static etc.
- Note: some examples in the pyiron_like_workflows.ipynb do not work (pyiron_workflow fails to connect some of the input and output ports, I was not able to figure out what is going wrong)
  • Loading branch information
JNmpi committed Feb 6, 2024
1 parent 4472a4f commit d6c57a9
Show file tree
Hide file tree
Showing 22 changed files with 7,654 additions and 3,023 deletions.
6,124 changes: 4,784 additions & 1,340 deletions notebooks/phonopy_wf.ipynb

Large diffs are not rendered by default.

3,400 changes: 1,826 additions & 1,574 deletions notebooks/pyiron_like_workflows.ipynb

Large diffs are not rendered by default.

113 changes: 110 additions & 3 deletions pyiron_workflow/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -973,19 +973,21 @@ def executor_shutdown(self, wait=True, *, cancel_futures=False):
except AttributeError:
pass

def iter(self, max_workers=1, cores_per_worker=1, executor=None, **kwargs):
def iter_old(self, max_workers=1, cores_per_worker=1, executor=None, **kwargs):
from pympipool import Executor
import pandas as pd

# Get the keys and lists from kwargs
keys = list(kwargs.keys())
lists = list(kwargs.values())
# print ('lists: ', lists)

# Get the number of dimensions
num_dimensions = len(keys)

# Get the length of each list
lengths = [len(lst) for lst in lists]
# print ('lengths: ', lengths, num_dimensions)

# Initialize indices
indices = [0] * num_dimensions
Expand All @@ -1001,6 +1003,7 @@ def iter(self, max_workers=1, cores_per_worker=1, executor=None, **kwargs):
# Perform multidimensional for loop
count = 0
while indices[0] < lengths[0]:
# print (f'iter: indices {indices}, {lengths[0]} {count}')
# Access the current elements using indices
current_elements = [lists[i][indices[i]] for i in range(num_dimensions)]

Expand All @@ -1023,8 +1026,11 @@ def iter(self, max_workers=1, cores_per_worker=1, executor=None, **kwargs):
iter_dict[count] = out
count += 1

for k, v in out.items():
current_elements_kwarg[k] = v
if hasattr(out, "items"):
for k, v in out.items():
current_elements_kwarg[k] = v
else:
current_elements_kwarg[self.label] = out

# Append the current_elements_kwarg to the dictionary
for k, v in current_elements_kwarg.items():
Expand All @@ -1038,11 +1044,112 @@ def iter(self, max_workers=1, cores_per_worker=1, executor=None, **kwargs):

# Update indices for the next iteration
indices[num_dimensions - 1] += 1
# print ('indices: ', indices)

# Update indices and carry-over if needed
for i in range(num_dimensions - 1, 0, -1):
# print ('dimensions: ', i, indices[i], lengths[i])
if indices[i] == lengths[i]:
indices[i] = 0
indices[i - 1] += 1

return pd.DataFrame(dict_lst)

def iter(self, max_workers=1, executor=None, **kwargs):
from concurrent.futures import ThreadPoolExecutor, as_completed
import pandas as pd

futures = []
future_index_map = {}
out = []
out_index = []

refs = to_list_of_kwargs(**kwargs)
df_refs = pd.DataFrame(refs)
# print("iter_refs: ", refs)
if max_workers < 2:
executor = None
else:
executor = max_workers

print("max_workers: ", max_workers)
if executor is None:
for i, ref in enumerate(refs):
out.append(self(**ref))
out_index.append(i)
else:
with ThreadPoolExecutor(max_workers=max_workers) as p:
for i, ref in enumerate(refs):
# use the class rather than the instance -> (type(self))
future = p.submit(func, type(self), **ref)
future_index_map[future] = i
futures.append(future)

for future in as_completed(futures):
out.append(future.result())
out_index.append(future_index_map[future])

if len(out) > 0:
if not hasattr(out[0], "items"):
print("iter: add label")
df_dict = {self.label: out}
else:
df_dict = {}
for i, row in enumerate(out):
for key, value in row.items():
if i == 0:
df_dict[key] = [value]
else:
df_dict[key].append(value)

df_out = pd.DataFrame(df_dict)

# try:
# df_out = pd.DataFrame(out, index=out_index).sort_index()
#
# except:
# print("iter out: ", out)
# return out

return pd.concat([df_refs, df_out], axis=1)


def to_list_of_kwargs(**kwargs):
keys = list(kwargs.keys())
lists = list(kwargs.values())

# Get the number of dimensions
num_dimensions = len(keys)

# Get the length of each list
lengths = [len(lst) for lst in lists]

# Initialize indices
indices = [0] * num_dimensions

kwargs_list = []

# Perform multidimensional for loop
while indices[0] < lengths[0]:
# Access the current elements using indices
current_elements = [lists[i][indices[i]] for i in range(num_dimensions)]

# Add current_elements as a dictionary
current_elements_kwarg = dict(zip(keys, current_elements))
kwargs_list.append(current_elements_kwarg)

# Update indices for the next iteration
indices[num_dimensions - 1] += 1

# Update indices and carry-over if needed
for i in range(num_dimensions - 1, 0, -1):
if indices[i] == lengths[i]:
indices[i] = 0
indices[i - 1] += 1

return kwargs_list


def func(node, **kwargs):
# print("func (node): ", node, kwargs)
return node(**kwargs).run()
Empty file.
Original file line number Diff line number Diff line change
@@ -1,43 +1,10 @@
import numpy as np

from pyiron_workflow.function import single_value_node, function_node
from typing import Optional, Union
from dataclasses import field

from pyiron_workflow.node_library.dev_tools import wf_data_class, wfMetaData


@wf_data_class()
class OutputCalcStatic:
from ase import Atoms

energy: Optional[float] = field(default=None, metadata=wfMetaData(log_level=0))
forces: Optional[np.ndarray] = field(default=None, metadata=wfMetaData(log_level=0))
stress: Optional[np.ndarray] = field(
default=None, metadata=wfMetaData(log_level=10)
)
structure: Optional[Atoms] = field(default=None, metadata=wfMetaData(log_level=10))

energies: Optional[float] = field(
default=None,
metadata=wfMetaData(
log_level=0, doc="per atom energy, only if supported by calculator"
),
)


@wf_data_class()
class OutputCalcMinimize:
initial: Optional[OutputCalcStatic] = field(
default_factory=lambda: OutputCalcStatic(), metadata=wfMetaData(log_level=0)
)
final: Optional[OutputCalcStatic] = field(
default_factory=lambda: OutputCalcStatic(), metadata=wfMetaData(log_level=0)
)


@single_value_node()
def static(atoms=None, engine=None, _internal=None, keys_to_store=None):
from pyiron_workflow.node_library.atomistic.calculator.data import OutputCalcStatic

if engine is None:
from ase.calculators.emt import EMT

Expand All @@ -47,8 +14,8 @@ def static(atoms=None, engine=None, _internal=None, keys_to_store=None):

out = OutputCalcStatic()
# out['structure'] = atoms # not needed since identical to input
out["energy"] = atoms.get_potential_energy()
out["forces"] = atoms.get_forces()
out.energy = atoms.get_potential_energy()
out.forces = atoms.get_forces()

if _internal is not None:
out["iter_index"] = _internal[
Expand All @@ -60,7 +27,11 @@ def static(atoms=None, engine=None, _internal=None, keys_to_store=None):
@function_node("structure", "out")
def minimize(atoms=None, engine=None, fmax=0.005, log_file="tmp.log"):
from ase.optimize import BFGS
import numpy as np
from pyiron_workflow.node_library.atomistic.calculator.data import (
OutputCalcMinimize,
)

# import numpy as np

if engine is None:
from ase.calculators.emt import EMT
Expand Down Expand Up @@ -97,5 +68,3 @@ def minimize(atoms=None, engine=None, fmax=0.005, log_file="tmp.log"):
static,
minimize,
]

from dataclasses import dataclass
142 changes: 142 additions & 0 deletions pyiron_workflow/node_library/atomistic/calculator/data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
from typing import Optional
from dataclasses import field

from pyiron_workflow.node_library.dev_tools import wf_data_class, wfMetaData


@wf_data_class()
class OutputCalcStatic:
from ase import Atoms
import numpy as np

energy_pot: Optional[float] = field(default=None, metadata=wfMetaData(log_level=0))
force: Optional[np.ndarray] = field(default=None, metadata=wfMetaData(log_level=0))
stress: Optional[np.ndarray] = field(
default=None, metadata=wfMetaData(log_level=10)
)
structure: Optional[Atoms] = field(default=None, metadata=wfMetaData(log_level=10))

atomic_energies: Optional[float] = field(
default=None,
metadata=wfMetaData(
log_level=0, doc="per atom energy, only if supported by calculator"
),
)


@wf_data_class()
class OutputCalcMinimize:
initial: Optional[OutputCalcStatic] = field(
default_factory=lambda: OutputCalcStatic(), metadata=wfMetaData(log_level=0)
)
final: Optional[OutputCalcStatic] = field(
default_factory=lambda: OutputCalcStatic(), metadata=wfMetaData(log_level=0)
)


@wf_data_class()
class OutputCalcMD:
import numpy as np

energies_pot: Optional[np.ndarray] = field(
default=None, metadata=wfMetaData(log_level=0)
)
energies_kin: Optional[np.ndarray] = field(
default=None, metadata=wfMetaData(log_level=0)
)
forces: Optional[np.ndarray] = field(default=None, metadata=wfMetaData(log_level=0))
positions: Optional[np.ndarray] = field(
default=None, metadata=wfMetaData(log_level=0)
)
temperatures: Optional[np.ndarray] = field(
default=None, metadata=wfMetaData(log_level=0)
)


@wf_data_class()
class InputCalcMD:
"""
Set an MD calculation within LAMMPS. Nosé Hoover is used by default.
Parameters
temperature (None/float/list) – Target temperature value(-s). If set to None, an NVE calculation is performed. It is required when the pressure is set or langevin is set It can be a list of temperature values, containing the initial target temperature and the final target temperature (in between the target value is varied linearly).
pressure (None/float/numpy.ndarray/list) – Target pressure. If set to None, an NVE or an NVT calculation is performed. A list of up to length 6 can be given to specify xx, yy, zz, xy, xz, and yz components of the pressure tensor, respectively. These values can mix floats and None to allow only certain degrees of cell freedom to change. (Default is None, run isochorically.)
n_ionic_steps (int) – Number of ionic steps
time_step (float) – Step size in fs between two steps.
n_print (int) – Print frequency
temperature_damping_timescale (float) – The time associated with the thermostat adjusting the temperature. (In fs. After rescaling to appropriate time units, is equivalent to Lammps’ Tdamp.)
pressure_damping_timescale (float) – The time associated with the barostat adjusting the temperature. (In fs. After rescaling to appropriate time units, is equivalent to Lammps’ Pdamp.)
seed (int) – Seed for the random number generation (required for the velocity creation)
tloop –
initial_temperature (None/float) – Initial temperature according to which the initial velocity field is created. If None, the initial temperature will be twice the target temperature (which would go immediately down to the target temperature as described in equipartition theorem). If 0, the velocity field is not initialized (in which case the initial velocity given in structure will be used). If any other number is given, this value is going to be used for the initial temperature.
langevin (bool) – (True or False) Activate Langevin dynamics
delta_temp (float) – Thermostat timescale, but in your Lammps time units, whatever those are. (DEPRECATED.)
delta_press (float) – Barostat timescale, but in your Lammps time units, whatever those are. (DEPRECATED.)
job_name (str) – Job name of the job to generate a unique random seed.
rotation_matrix (numpy.ndarray) – The rotation matrix from the pyiron to Lammps coordinate frame.
"""

temperature: float = 300
n_ionic_steps: int = 1_000
n_print: int = 100
pressure = None
time_step: float = 1.0
temperature_damping_timescale: float = 100.0
pressure_damping_timescale: float = 1000.0
seed = None
tloop = None
initial_temperature = None
langevin = False
delta_temp = None
delta_press = None


@wf_data_class()
class InputCalcMinimize:
"""
Sets parameters required for minimization.
Parameters
e_tol (float) – If the magnitude of difference between energies of two consecutive steps is lower than or equal to e_tol, the minimisation terminates. (Default is 0.0 eV.)
f_tol (float) – If the magnitude of the global force vector at a step is lower than or equal to f_tol, the minimisation terminates. (Default is 1e-4 eV/angstrom.)
max_iter (int) – Maximum number of minimisation steps to carry out. If the minimisation converges before max_iter steps, terminate at the converged step. If the minimisation does not converge up to max_iter steps, terminate at the max_iter step. (Default is 100000.)
pressure (None/float/numpy.ndarray/list) – Target pressure. If set to None, an NVE or an NVT calculation is performed. A list of up to length 6 can be given to specify xx, yy, zz, xy, xz, and yz components of the pressure tensor, respectively. These values can mix floats and None to allow only certain degrees of cell freedom to change. (Default is None, run isochorically.)
n_print (int) – Write (dump or print) to the output file every n steps (Default: 100)
style ('cg'/'sd'/other values from Lammps docs) – The style of the numeric minimization, either conjugate gradient, steepest descent, or other keys permissible from the Lammps docs on ‘min_style’. (Default is ‘cg’ – conjugate gradient.)
rotation_matrix (numpy.ndarray) – The rotation matrix from the pyiron to Lammps coordinate frame.
"""

e_tol: float = 0.0
f_tol: float = 1e-4
max_iter: int = 1_000_000
pressure: float = None
n_print: int = 100
style: str = "cg"


@wf_data_class()
class InputCalcStatic:
keys_to_store: Optional[list] = field(default_factory=list)


nodes = []
18 changes: 18 additions & 0 deletions pyiron_workflow/node_library/atomistic/calculator/generic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from pyiron_workflow.function import single_value_node

from pyiron_workflow.node_library.atomistic.calculator.data import (
InputCalcMinimize,
InputCalcMD,
InputCalcStatic,
)


@single_value_node("generic")
def static(structure=None, engine=None, keys_to_store=None):
output = engine(
structure=structure, calculator=InputCalcStatic(keys_to_store=keys_to_store)
)
return output.generic


nodes = [static]
Empty file.
File renamed without changes.
Loading

0 comments on commit d6c57a9

Please sign in to comment.