Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ae170cd
slight server tweaks
pdobbelaere Jul 15, 2025
05fd6cd
imports
pdobbelaere Sep 3, 2025
f586193
Update server.py
pdobbelaere Sep 8, 2025
fd30382
i-pi driver
pdobbelaere Sep 8, 2025
4df8a28
Update execution.py
pdobbelaere Nov 14, 2025
4c04d6a
update ASE optimisation code
pdobbelaere Feb 16, 2026
aa385dd
use built-in custom driver support
pdobbelaere Feb 17, 2026
95896fc
Update metadynamics.py
pdobbelaere Feb 17, 2026
b19a64b
deprecations
pdobbelaere Feb 17, 2026
f49772e
minor fixes
pdobbelaere Feb 17, 2026
2136d00
sampling overhaul
pdobbelaere Feb 19, 2026
754f8ce
enable fixed volume NPT
pdobbelaere Feb 20, 2026
8c730c0
dataclass Walker
pdobbelaere Feb 20, 2026
8317718
make timeconstants a settable option
pdobbelaere Feb 20, 2026
07bde4e
fix MD timeout
pdobbelaere Feb 20, 2026
e3d404b
guard against molecule + barostat MD
pdobbelaere Feb 20, 2026
38bb727
catch (some) exploding simulations
pdobbelaere Feb 20, 2026
7e07e8c
fix Walker.multiply
pdobbelaere Feb 23, 2026
003ff19
fix reading geometry containing lattice NaNs
pdobbelaere Feb 23, 2026
c66f7c5
tweak Walker reset functionality
pdobbelaere Feb 23, 2026
778e4c5
cleanup unused code
pdobbelaere Feb 23, 2026
4ce410a
bugfix REX MD
pdobbelaere Feb 23, 2026
3de338e
Update phonons.py
pdobbelaere Feb 23, 2026
ef2d5cf
remove typeguard
pdobbelaere Feb 23, 2026
4351712
Update test_sampling.py
pdobbelaere Feb 23, 2026
1bf22cc
small bugfixes
pdobbelaere Feb 23, 2026
3f8d233
bump version and update to latest ipi
pdobbelaere Feb 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ __pycache__/
*.py[cod]
*$py.class
*.swp
*.pyc

# C extensions
*.so
Expand Down
38 changes: 0 additions & 38 deletions psiflow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,41 +34,3 @@ def resolve_and_check(path: Path) -> Path:
context = ExecutionContextLoader.context
wait = ExecutionContextLoader.wait


# TODO: EXECUTION
# - max_runtime is in seconds, max_simulation_time (and others) is in minutes
# - ExecutionDefinition gpu argument?
# - ExecutionDefinition wrap_in_timeout functionality
# - ExecutionDefinition wrap_in_srun functionality? Actually for MD this is more iffy right
# - ExecutionDefinition why properties?
# - ExecutionDefinition centralize wq_resources
# - configuration file with all options
# - timeout -s 9 or -s 15?
# - executor keys are hardcoded strings..
# - define a 'format_env_variables' util
# - update GPAW + ORCA + Default containers
# include s-dftd3 in modelevaluation + install s-dftd3 with openmp
# - what with mem_per_node / mem_per_worker
# - always GPU for training?
# - currently reference mpi_args have to be tuple according to typeguard..
# - cores_per_block has to be specified even when exclusive..?
# - can we do something with WQ priority?
# - see chatgpt convo for process memory limits and such
# - make /tmp for app workdirs an option?
# - what is scaling_cores_per_worker in WQ
# - can we clean up psiflow_internal slightly?
# -
# TODO: REFERENCE
# - reference MPI args not really checked
# - mpi flags are very finicky across implementations --> use ENV args?
# OMPI_MCA_orte_report_bindings=1 I_MPI_DEBUG=4
# - commands ends with 'exit 0' - what if we do not want to exit yet?
# - some actual logging?
# - safe_compute_dataset functionality?
# - ReferenceDummy is a bit sloppy
# -
# TODO: MISC
# - think about test efficiency
# - some imports take a very long time
# - fix serialisation
# -
56 changes: 33 additions & 23 deletions psiflow/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from threading import Lock

# see https://stackoverflow.com/questions/59904631/python-class-constants-in-dataclasses
from typing import Any, Optional, Union, ClassVar, Protocol
from typing import Any, Optional, Union, ClassVar, Protocol, Iterable

import parsl
import psutil
Expand Down Expand Up @@ -155,7 +155,7 @@ def create_executor(self, path: Path) -> ParslExecutor:

# only ModelEvaluation / ModelTraining / default_htex run in containers
if not isinstance(self, ReferenceEvaluation) and self.container:
prepend = self.container.launch_command()
prepend = self.container.launch_command(self.gpu)
worker_executable = f"{prepend} work_queue_worker"
else:
worker_executable = "work_queue_worker"
Expand Down Expand Up @@ -261,27 +261,37 @@ def client_command(self):
command_list = ["psiflow-client"]
return " ".join(command_list)

def get_client_args(
self,
hamiltonian_name: str,
nwalkers: int,
motion: str,
) -> list[str]:
if "MACE" in hamiltonian_name:
if motion in ["minimize", "vibrations"]:
dtype = "float64"
else:
dtype = "float32"
nclients = min(nwalkers, self.max_workers)
if self.gpu:
template = "--dtype={} --device=cuda:{}"
args = [template.format(dtype, i) for i in range(nclients)]
else:
template = "--dtype={} --device=cpu"
args = [template.format(dtype) for i in range(nclients)]
return args
# def get_client_args(
# self,
# hamiltonian_name: str,
# nwalkers: int,
# motion: str,
# ) -> list[str]:
# # TODO: redo this
# if "MACE" in hamiltonian_name:
# if motion in ["minimize", "vibrations"]:
# dtype = "float64"
# else:
# dtype = "float32"
# nclients = min(nwalkers, self.max_workers)
# if self.gpu:
# template = "--dtype={} --device=cuda:{}"
# args = [template.format(dtype, i) for i in range(nclients)]
# else:
# template = "--dtype={} --device=cpu"
# args = [template.format(dtype) for i in range(nclients)]
# return args
# else:
# return [""]

def get_driver_devices(self, nwalkers: int) -> list[dict]:
# assumes driver is GPU capable
# TODO: what if only 1 gpu is available?
nclients = min(nwalkers, self.max_workers)
if self.gpu:
return [{'device': f'cuda:{i}'} for i in range(nclients)]
else:
return [""]
return [{'device': 'cpu'} for _ in range(nclients)]

def wq_resources(self, nwalkers):
if self.use_threadpool:
Expand Down Expand Up @@ -433,7 +443,7 @@ class ReferenceSpec(Protocol):
name: ClassVar[str]
reference_args: ClassVar[tuple[str, ...]]
mpi_command: str
mpi_args: tuple[str, ...]
mpi_args: Iterable[str]
executable: str

def launch_command(self) -> str:
Expand Down
105 changes: 41 additions & 64 deletions psiflow/free_energy/phonons.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,28 @@
from __future__ import annotations # necessary for type-guarding class methods

import xml.etree.ElementTree as ET
from typing import Optional, Union

import numpy as np
import parsl
import typeguard
from ase.units import Bohr, Ha, J, _c, _hplanck, _k, kB, second
from parsl.app.app import bash_app, python_app
from parsl.dataflow.futures import AppFuture

import psiflow
from psiflow.data import Dataset
from psiflow.geometry import Geometry, mass_weight
from psiflow.hamiltonians import Hamiltonian, MixtureHamiltonian
from psiflow.hamiltonians import Hamiltonian, MACEHamiltonian
from psiflow.sampling.sampling import (
setup_sockets,
make_force_xml,
make_start_command,
make_client_command
make_server_command,
make_driver_commands,
make_wait_for_sockets_command,
)
from psiflow.sampling.optimize import setup_forces, export_env_command
from psiflow.utils.apps import multiply
from psiflow.utils.io import load_numpy, save_xml
from psiflow.utils import TMP_COMMAND, CD_COMMAND


@typeguard.typechecked
def _compute_frequencies(hessian: np.ndarray, geometry: Geometry) -> np.ndarray:
assert hessian.shape[0] == hessian.shape[1]
assert len(geometry) * 3 == hessian.shape[0]
Expand All @@ -35,7 +32,6 @@ def _compute_frequencies(hessian: np.ndarray, geometry: Geometry) -> np.ndarray:
compute_frequencies = python_app(_compute_frequencies, executors=["default_threads"])


@typeguard.typechecked
def _harmonic_free_energy(
frequencies: Union[float, np.ndarray],
temperature: float,
Expand Down Expand Up @@ -65,9 +61,7 @@ def _harmonic_free_energy(
harmonic_free_energy = python_app(_harmonic_free_energy, executors=["default_threads"])


@typeguard.typechecked
def setup_motion(
mode: str,
asr: str,
pos_shift: float,
energy_shift: float,
Expand All @@ -91,43 +85,40 @@ def setup_motion(


def _execute_ipi(
hamiltonian_names: list[str],
client_args: list[list[str]],
driver_kwargs: list[dict],
command_server: str,
command_client: str,
stdout: str = "",
stderr: str = "",
env_vars: dict = {},
stdout: str = parsl.AUTO_LOGNAME,
stderr: str = parsl.AUTO_LOGNAME,
inputs: list = [],
outputs: list = [],
parsl_resource_specification: Optional[dict] = None,
) -> str:
command_start = make_start_command(command_server, inputs[0], inputs[1])
commands_client = []
for i, name in enumerate(hamiltonian_names):
args = client_args[i]
assert len(args) == 1 # only have one client per hamiltonian
for arg in args:
commands_client += make_client_command(command_client, name, inputs[2 + i], inputs[1], arg),

command_end = f'{command_server} --cleanup'
command_copy = f'cp i-pi.output_full.hess {outputs[0]}'
file_xml, file_xyz_in, *files_in = inputs
command_start = make_server_command(
command_server, file_xml, file_xyz_in, outputs[0], [], []
)
command_wait = make_wait_for_sockets_command(
set(d["address"] for d in driver_kwargs)
)
commands_driver = make_driver_commands(driver_kwargs, file_xyz_in, files_in)

command_list = [
TMP_COMMAND,
CD_COMMAND,
export_env_command(env_vars),
command_start,
*commands_client,
command_wait,
*commands_driver,
"wait",
command_end,
command_copy,
f"cp i-pi.output_full.hess {outputs[0]}",
]
return "\n".join(command_list)


execute_ipi = bash_app(_execute_ipi, executors=["ModelEvaluation"])


@typeguard.typechecked
def compute_harmonic(
state: Union[Geometry, AppFuture],
hamiltonian: Hamiltonian,
Expand All @@ -136,26 +127,23 @@ def compute_harmonic(
pos_shift: float = 0.01,
energy_shift: float = 0.00095,
) -> AppFuture:
hamiltonian: MixtureHamiltonian = 1 * hamiltonian
names = hamiltonian.get_named_components()
sockets = setup_sockets(names)
forces = make_force_xml(hamiltonian, names)
components, force_xml = setup_forces(hamiltonian)
sockets = setup_sockets(components)

initialize = ET.Element("initialize", nbeads="1")
start = ET.Element("file", mode="ase", cell_units="angstrom")
start.text = " start_0.xyz "
initialize.append(start)
motion = setup_motion(mode, asr, pos_shift, energy_shift)
motion = setup_motion(asr, pos_shift, energy_shift)
if mode != "fd":
raise NotImplementedError

system = ET.Element("system")
system.append(initialize)
system.append(motion)
system.append(forces)

# output = setup_output(keep_trajectory)
system.append(force_xml)

simulation = ET.Element("simulation", mode="static")
# simulation.append(output)
for socket in sockets:
simulation.append(socket)
simulation.append(system)
Expand All @@ -169,33 +157,22 @@ def compute_harmonic(
simulation,
outputs=[context.new_file("input_", ".xml")],
).outputs[0]
inputs = [
input_future,
Dataset([state]).extxyz,
]
inputs += hamiltonian.serialize(dtype="float64")

client_args = []
for name in names:
args = definition.get_client_args(name, 1, "vibrations")
client_args.append(args)
outputs = [
context.new_file("hess_", ".txt"),
]
inputs = [input_future, Dataset([state]).extxyz]

command_server = definition.server_command()
command_client = definition.client_command()
resources = definition.wq_resources(1)
driver_kwargs = []
for i, comp in enumerate(components):
inputs.append(comp.hamiltonian.serialize_function(dtype="float64"))
kwargs = {"idx": i, "address": comp.address}
if isinstance(comp.hamiltonian, MACEHamiltonian):
kwargs |= definition.get_driver_devices(1)[0]
driver_kwargs.append(kwargs)

result = execute_ipi(
names,
client_args,
command_server,
command_client,
stdout=parsl.AUTO_LOGNAME,
stderr=parsl.AUTO_LOGNAME,
driver_kwargs,
definition.server_command(),
env_vars=definition.env_vars,
inputs=inputs,
outputs=outputs,
parsl_resource_specification=resources,
outputs=[context.new_file("hess_", ".txt")],
parsl_resource_specification=definition.wq_resources(1),
)
return multiply(load_numpy(inputs=[result.outputs[0]]), Ha / Bohr**2)
return multiply(load_numpy(inputs=[result.outputs[0]]), Ha / Bohr**2)
9 changes: 8 additions & 1 deletion psiflow/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,9 +304,16 @@ def from_string(cls, s: str, natoms: Optional[int] = None) -> Optional[Geometry]
if key.startswith("order_"):
order[key.replace("order_", "")] = value

# TODO: this needs a better solution
cell = comment_dict.pop("Lattice", np.zeros((3, 3)))
if isinstance(cell, str):
# most likely an array of NaNs
cell = np.array(cell.split()).reshape((3, 3))
cell = cell.T # transposed!
pbc = sum(comment_dict.pop("pbc", [])) == 3
geometry = cls(
per_atom=per_atom,
cell=comment_dict.pop("Lattice", np.zeros((3, 3))).T, # transposed!
cell=cell if pbc else np.zeros((3, 3)),
energy=comment_dict.pop("energy", None),
stress=comment_dict.pop("stress", None),
delta=comment_dict.pop("delta", None),
Expand Down
Loading
Loading