Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
76e7c30
exec-runner part 0: baseline capture
srmnitc Jul 8, 2026
291b688
exec-runner part 1: decoupling micro-changes
srmnitc Jul 8, 2026
22251e7
exec-runner part 2: RecordingRunner + golden command streams
srmnitc Jul 9, 2026
deca107
exec-runner part 3: ExecutableRunner + SessionState
srmnitc Jul 14, 2026
864b3bc
exec-runner part 4: preflight capability check
srmnitc Jul 14, 2026
a48a902
exec-runner part 5: wire-in and pylammpsmpi removal
srmnitc Jul 14, 2026
0f28ea4
exec-runner part 6: real-LAMMPS integration tests
srmnitc Jul 14, 2026
53b447a
exec-runner part 7: documentation - drive the lmp executable
srmnitc Jul 14, 2026
72df1f2
fix error in error
srmnitc Jul 14, 2026
b54c04d
output: dissipation in report.yaml + column-legend headers on data files
srmnitc Jul 14, 2026
6fbb61e
docs: output-file reference page + lambda_schedule clarification
srmnitc Jul 14, 2026
82383dc
feat: port range_scan module (prescan phase-transition detection)
srmnitc Jul 14, 2026
bfe11b9
exec-runner part 8: BaseRunner contract - route all reads through the…
srmnitc Jul 16, 2026
3b14db6
exec-runner part 9: LibraryRunner - optional pylammpsmpi backend
srmnitc Jul 16, 2026
86cbfff
exec-runner part 10: execution_mode wire-in, optional dependency, bac…
srmnitc Jul 16, 2026
dfd9906
docs: LAMMPS installation - library-backend setup instructions
srmnitc Jul 18, 2026
03ec466
Merge upstream main (1.8.5): box-command removal, MLIAP support, depe…
srmnitc Jul 18, 2026
d474eb9
ci: drop stale pylammpsmpi steps, add library-backend smoke job
srmnitc Jul 18, 2026
c2e338e
input: strict key validation with hints; remove legacy converter
srmnitc Jul 18, 2026
47598f2
input: fix md damping field types - drop the dead Union list branch
srmnitc Jul 18, 2026
baa7050
md.seed: one master seed for every stochastic step, always recorded
srmnitc Jul 18, 2026
64a853e
docs examples: drop phase_transition_detection blocks, refresh exampl…
srmnitc Jul 18, 2026
90a93e3
phase_diagram: make the TDB round-trip reproduce the calphy diagram i…
srmnitc Jul 19, 2026
b20fd0f
ci: drop LAMMPS from hosted CI; run pip-only "not lammps" lane, remov…
srmnitc Jul 20, 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
38 changes: 22 additions & 16 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,30 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Setup Miniforge
uses: conda-incubator/setup-miniconda@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
miniforge-version: latest
channels: conda-forge
environment-file: environment.yml

- name: Upgrade pylammpsmpi
shell: bash -l {0}
run: pip install --upgrade pylammpsmpi
cache: pip
cache-dependency-path: pyproject.toml

# Every runtime dependency (numpy, scipy, pyscal3, mendeleev, ...) ships
# wheels on PyPI, so plain pip is enough -- no conda env-solve, no LAMMPS.
- name: Install calphy
shell: bash -l {0}
run: |
pip install -e ".[test]"
pip install --upgrade pylammpsmpi
run: pip install -e ".[test]"

- name: Run tests
shell: bash -l {0}
run: pytest tests/ -v
# The LAMMPS integration suite (tests/test_integration_lammps.py, marked
# `lammps`) drives a real `lmp` binary end to end -- too slow and fragile
# for hosted CI, so it is deliberately excluded here. Run it locally
# before a release against a LAMMPS-enabled environment:
#
# conda env create -f environment.yml # ships conda-forge lammps
# pytest -m lammps -v
#
# See tests/baselines/BASELINE_INFO.md for the frozen reference results.
# Everything the excluded suite does NOT cover -- the exact command stream
# calphy emits (golden tests), the dry-run runner, the fake-pylammpsmpi
# library backend, input validation, and all pure-python paths -- needs no
# binary and runs on every push below.
- name: Run tests (no LAMMPS binary required)
run: pytest tests/ -m "not lammps" -v
24 changes: 0 additions & 24 deletions .gitlab-ci.yml

This file was deleted.

754 changes: 754 additions & 0 deletions EXECUTABLE_RUNNER_PLAN.md

Large diffs are not rendered by default.

18 changes: 11 additions & 7 deletions calphy.def
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,26 @@ From: ubuntu:22.04
python3.8 -m venv /singularity-venv
. /singularity-venv/bin/activate

# Install packages
pip install numpy==1.24.4 ase==3.23.0 mpi4py==3.1.5
pip install mendeleev==0.17.0 pylammpsmpi==0.2.16 pyscal==0.12 pyyaml==6.0.2 scipy==1.10.1 tqdm==4.66.2
# Install packages (calphy drives the external `lmp` binary directly, so no
# pylammpsmpi / mpi4py / LAMMPS python library is needed).
pip install numpy==1.24.4 ase==3.23.0
pip install mendeleev==0.17.0 pyscal==0.12 pyyaml==6.0.2 scipy==1.10.1 tqdm==4.66.2
pip install calphy

# Clone lammps source and make
# Clone lammps source and build the `lmp` executable with the packages
# calphy needs (EXTRA-FIX: ti/spring, EXTRA-PAIR: ufm/hybrid-scaled,
# MC: atom/swap, QTB: fix qtb). Put the binary on PATH as `lmp`.
git clone https://github.com/lammps/lammps.git --branch patch_2Aug2023_update3
cd /lammps/src/
sed -i '35s/.*/LMP_INC = -DLAMMPS_GZIP -DLAMMPS_JPEG -DLAMMPS_PNG -DLAMMPS_FFMPEG -DLAMMPS_EXCEPTIONS/' MAKE/MACHINES/Makefile.ubuntu
make yes-manybody
make yes-meam
make yes-EXTRA-FIX
make yes-EXTRA-PAIR
make -j 8 ubuntu mode=shlib
make install-python
cp liblammps_ubuntu.so /singularity-venv/lib/python3.8/site-packages/lammps
make yes-MC
make yes-QTB
make -j 8 ubuntu
cp lmp_ubuntu /usr/local/bin/lmp
cd ../../
%runscript

Expand Down
68 changes: 16 additions & 52 deletions calphy/alchemy.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ class Alchemy(cph.Phase):

"""

def __init__(self, calculation=None, simfolder=None, log_to_screen=False, lmp=None):
def __init__(self, calculation=None, simfolder=None, log_to_screen=False):

# call base class
super().__init__(
calculation=calculation, simfolder=simfolder, log_to_screen=log_to_screen, lmp=lmp,
calculation=calculation, simfolder=simfolder, log_to_screen=log_to_screen,
)

def run_averaging(self):
Expand All @@ -81,15 +81,7 @@ def run_averaging(self):
Fix lattice option is not implemented at present.
At the end of the run, the averaged box dimensions are calculated.
"""
lmp = ph.create_object(
cores=self.cores,
directory=self.simfolder,
timestep=self.calc.md.timestep,
cmdargs=self.calc.md.cmdargs,
init_commands=self.calc.md.init_commands,
script_mode=self.calc.script_mode,
lmp=self._lmp,
)
lmp = ph.create_object(self.calc, self.simfolder)

lmp.command(f"pair_style {self.calc._pair_style_with_options[0]}")

Expand Down Expand Up @@ -125,27 +117,15 @@ def run_averaging(self):
# routine in which lattice constant will not varied, but is set to a given fixed value
self.run_constrained_pressure_convergence(lmp)

if not self.calc.script_mode:
# check for melting
self.dump_current_snapshot(lmp, "traj.equilibration_stage2.dat")
self.check_if_melted(lmp, "traj.equilibration_stage2.dat")
# check for melting
self.dump_current_snapshot(lmp, "traj.equilibration_stage2.dat")
self.check_if_melted(lmp, "traj.equilibration_stage2.dat")

# close object and process traj
lmp = ph.write_data(lmp, "conf.equilibration.data")

if self.calc.script_mode:
file = os.path.join(self.simfolder, "averaging.lmp")
lmp.write(file)
return

self.lammps_close(lmp=lmp)
# Preserve log file
logfile = os.path.join(self.simfolder, "log.lammps")
try:
if os.path.exists(logfile):
os.rename(logfile, os.path.join(self.simfolder, "averaging.log.lammps"))
except OSError as e:
self.logger.warning(f"Failed to rename log file: {e}")
lmp.rotate_logs("averaging")

def run_integration(self, iteration=1):
"""
Expand All @@ -166,15 +146,7 @@ def run_integration(self, iteration=1):
the lambda parameter. See algorithm 4 in publication.
"""
# create lammps object
lmp = ph.create_object(
cores=self.cores,
directory=self.simfolder,
timestep=self.calc.md.timestep,
cmdargs=self.calc.md.cmdargs,
init_commands=self.calc.md.init_commands,
script_mode=self.calc.script_mode,
lmp=self._lmp,
)
lmp = ph.create_object(self.calc, self.simfolder)

# Adiabatic switching parameters.
lmp.command("variable li equal 1.0")
Expand Down Expand Up @@ -379,7 +351,9 @@ def run_integration(self, iteration=1):

# save the necessary items to a file: first step
lmp.command(
'fix f2 all print 1 "${dU1} ${dU2} ${flambda}" screen no file forward_%d.dat'
'fix f2 all print 1 "${dU1} ${dU2} ${flambda}" '
'title "# dU_1[eV/atom] dU_2[eV/atom] lambda" '
"screen no file forward_%d.dat"
% iteration
)
lmp.command("run %d" % self.calc._n_switching_steps)
Expand Down Expand Up @@ -498,7 +472,9 @@ def run_integration(self, iteration=1):

# save the necessary items to a file: first step
lmp.command(
'fix f2 all print 1 "${dU1} ${dU2} ${flambda}" screen no file backward_%d.dat'
'fix f2 all print 1 "${dU1} ${dU2} ${flambda}" '
'title "# dU_1[eV/atom] dU_2[eV/atom] lambda" '
"screen no file backward_%d.dat"
% iteration
)
lmp.command("run %d" % self.calc._n_switching_steps)
Expand All @@ -515,21 +491,8 @@ def run_integration(self, iteration=1):
lmp.command(f"unfix swap{idx}")
# lmp.command("unfix swap_print")

if self.calc.script_mode:
file = os.path.join(self.simfolder, "integration.lmp")
lmp.write(file)
return

self.lammps_close(lmp=lmp)
# Preserve log file
logfile = os.path.join(self.simfolder, "log.lammps")
try:
if os.path.exists(logfile):
os.rename(
logfile, os.path.join(self.simfolder, "integration.log.lammps")
)
except OSError as e:
self.logger.warning(f"Failed to rename log file: {e}")
lmp.rotate_logs("integration")

def thermodynamic_integration(self):
"""
Expand All @@ -551,6 +514,7 @@ def thermodynamic_integration(self):
w, q, qerr = find_w(self.simfolder, self.calc, full=True, solid=False)

self.w = w
self.qdiss = q
self.ferr = qerr
self.fe = self.w

Expand Down
112 changes: 1 addition & 111 deletions calphy/clitools.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,119 +7,9 @@
import time
import datetime

from calphy.input import read_inputfile, load_job, save_job, _convert_legacy_inputfile
from calphy.liquid import Liquid
from calphy.solid import Solid
from calphy.alchemy import Alchemy
from calphy.input import read_inputfile
from calphy.phase_diagram import prepare_inputs_for_phase_diagram

def _generate_job(calc, simfolder):
if calc.mode == "alchemy" or calc.mode == "composition_scaling":
job = Alchemy(calculation=calc, simfolder=simfolder)
return job
else:
if calc.reference_phase == "liquid":
job = Liquid(calculation=calc, simfolder=simfolder)
return job
else:
job = Solid(calculation=calc, simfolder=simfolder)
return job


def run_averaging():
arg = ap.ArgumentParser()
arg.add_argument("-i", "--input", required=True, type=str,
help="name of the input file")
arg.add_argument("-k", "--kernel", required=True, type=int,
help="kernel number of the calculation to be run.")
args = vars(arg.parse_args())
kernel = args["kernel"]
calculations = read_inputfile(args["input"])
calc = calculations[kernel]

simfolder = calc.create_folders()
job = _generate_job(calc, simfolder)
os.chdir(simfolder)


job.run_averaging()
save_job(job)


def process_averaging():
arg = ap.ArgumentParser()
arg.add_argument("-i", "--input", required=True, type=str,
help="name of the input file")
arg.add_argument("-k", "--kernel", required=True, type=int,
help="kernel number of the calculation to be run.")
args = vars(arg.parse_args())
kernel = args["kernel"]
calculations = read_inputfile(args["input"])
calc = calculations[kernel]

job = load_job(calc.savefile)
job.process_averaging_results()
save_job(job)

def run_integration():
arg = ap.ArgumentParser()
arg.add_argument("-i", "--input", required=True, type=str,
help="name of the input file")
arg.add_argument("-k", "--kernel", required=True, type=int,
help="kernel number of the calculation to be run.")
args = vars(arg.parse_args())
kernel = args["kernel"]
calculations = read_inputfile(args["input"])
calc = calculations[kernel]

job = load_job(calc.savefile)
job.run_integration()
save_job(job)

def process_integration():
arg = ap.ArgumentParser()
arg.add_argument("-i", "--input", required=True, type=str,
help="name of the input file")
arg.add_argument("-k", "--kernel", required=True, type=int,
help="kernel number of the calculation to be run.")
args = vars(arg.parse_args())
kernel = args["kernel"]
calculations = read_inputfile(args["input"])
calc = calculations[kernel]

job = load_job(calc.savefile)
job.thermodynamic_integration()
job.submit_report()
save_job(job)

def convert_legacy_inputfile():
arg = ap.ArgumentParser()
arg.add_argument("-i", "--input", required=True, type=str,
help="name of the input file")
arg.add_argument("-s", "--split", required=False, type=bool,
help="split each calculation into new file.", default=True)
arg.add_argument("-o", "--output", required=False, type=str,
help="output file string, calculations will be named <outputstring>.*.yaml",
default='input')
args = vars(arg.parse_args())
calculations = _convert_legacy_inputfile(args['input'], return_calcs=True)
outputstr = args['output']

if args['split']:
#now we have to write this out to file
for count, calc in enumerate(calculations):
data = {}
data['calculations'] = [calc]
outfile = ".".join([outputstr, str(count+1), 'yaml'])
with open(outfile, 'w') as fout:
yaml.safe_dump(data, fout)
else:
data = {}
data['calculations'] = calculations
outfile = ".".join([outputstr, 'yaml'])
with open(outfile, 'w') as fout:
yaml.safe_dump(data, fout)


def phase_diagram():
arg = ap.ArgumentParser()
Expand Down
20 changes: 19 additions & 1 deletion calphy/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,22 @@ class SolidifiedError(CustomError):
class PhaseTransitionError(CustomError):
"""Raised when the pre-flight temperature-range scan flags a phase transition."""
pass



class LammpsExecutionError(RuntimeError):
"""LAMMPS binary exited abnormally.

Carries the segment script path, the segment log path, and a short excerpt of
the log (or stderr) so the failure can be diagnosed without re-running.
"""
pass


class RunnerStateError(RuntimeError):
"""Invalid command sequence for segmented execution.

Raised for an unknown command, a dump left live across a segment boundary, an
immediate-evaluation variable crossing a boundary, or a ``fix print`` being
replayed -- anything the ExecutableRunner cannot faithfully reproduce.
"""
pass
Loading
Loading