# Option 1: conda (recommended)
conda env create -f environment.yml
conda activate ams
pip install -e .
# Option 2: pip only
pip install -e .Verify installation:
from amstools import MurnaghanCalculator
from ase.build import bulk
from ase.calculators.emt import EMT
atoms = bulk('Al', 'fcc')
atoms.calc = EMT()
m = MurnaghanCalculator(atoms)
m.calculate()
print("Setup works!", m.value)# All tests
python -m pytest test/
# Single test file
python -m pytest test/test_murnaghanCalculator.py
# Single test function
python -m pytest test/test_murnaghanCalculator.py::test_fit_murnaghan_run
# Verbose with printed output
python -m pytest test/ -v -sTests use EMT (a fast toy potential) so they run in seconds without any DFT setup.
This is the most common contribution: adding a calculator for a new material property.
Create a new file in amstools/properties/, e.g. amstools/properties/my_property.py.
Subclass GeneralCalculator and implement three methods:
import logging
from collections import OrderedDict
from amstools.properties.generalcalculator import GeneralCalculator
class MyPropertyCalculator(GeneralCalculator):
"""Calculate some material property.
:param atoms: ASE Atoms object with calculator attached
:param my_param: Description of your parameter (default: 10)
Usage:
>>> atoms.calc = calculator
>>> calc = MyPropertyCalculator(atoms, my_param=10)
>>> calc.calculate()
>>> print(calc.value)
"""
property_name = "my_property"
# List ALL custom __init__ parameter names here (not atoms/calculator).
# This drives serialization and get_params_dict().
param_names = ["my_param"]
def __init__(self, atoms=None, my_param=10, **kwargs):
GeneralCalculator.__init__(self, atoms, **kwargs)
self.my_param = my_param
def generate_structures(self, verbose=False):
"""Create the structures to be calculated.
Returns an OrderedDict of {name: ASE Atoms}.
self.basis_ref is the input structure (a copy of the original atoms).
"""
structures = OrderedDict()
# Example: generate scaled structures
for i in range(self.my_param):
s = self.basis_ref.copy()
# ... modify structure ...
structures[f"config_{i}"] = s
return structures
def get_structure_value(self, structure, name=None):
"""Run calculation on a single structure.
Args:
structure: ASE Atoms with calculator already attached
name: the key from generate_structures()
Returns:
(result_dict, output_structure)
"""
energy = structure.get_potential_energy()
volume = structure.get_volume()
return {"energy": energy, "volume": volume}, structure
def analyse_structures(self, output_dict):
"""Aggregate results from all structures into self._value.
Args:
output_dict: {name: result_dict} from get_structure_value()
"""
energies = [v["energy"] for v in output_dict.values()]
self._value["energies"] = energies
self._value["min_energy"] = min(energies)Reference: amstools/properties/static.py is the simplest real example (~50 lines).
Add your class to amstools/properties/__init__.py:
from amstools.properties.my_property import MyPropertyCalculatorAnd to amstools/__init__.py's __all__ list:
__all__ = [
...
"MyPropertyCalculator",
]Create test/test_myPropertyCalculator.py:
import pytest
from amstools import MyPropertyCalculator
from test.utils import atoms, calculator
@pytest.fixture
def my_calc(atoms, calculator):
atoms.calc = calculator
return MyPropertyCalculator(atoms)
def test_calculate(my_calc):
my_calc.calculate()
assert "min_energy" in my_calc.value
def test_generate_structures(my_calc):
structures = my_calc.generate_structures()
assert len(structures) == 10 # default my_param
def test_to_from_dict(my_calc):
"""Verify serialization roundtrip works."""
my_calc.calculate()
d = my_calc.todict()
restored = MyPropertyCalculator.fromdict(d)
assert restored.value == my_calc.valueRun your tests: python -m pytest test/test_myPropertyCalculator.py -v
The todict()/fromdict() roundtrip is handled by GeneralCalculator automatically if you list all custom parameters in param_names. If your parameter contains non-JSON-serializable types (numpy arrays, custom objects), you may need to override todict()/fromdict().
| Method | Purpose | Input | Output |
|---|---|---|---|
generate_structures() |
Create structure variants | self.basis_ref (the input atoms) |
OrderedDict{name: Atoms} |
get_structure_value() |
Calculate one structure | single Atoms with .calc set |
(result_dict, Atoms) |
analyse_structures() |
Aggregate all results | {name: result_dict} |
writes to self._value |
self.value(orself._value) — the final computed properties (OrderedDict)self.output_structures_dict— output structures keyed by nameself.basis_ref— the input structure (copy of original atoms)self.calculator— the ASE calculator
Your calculator works standalone and in pipelines with no extra code:
from amstools import Pipeline, MyPropertyCalculator, MurnaghanCalculator
pipeline = Pipeline(
steps=[MurnaghanCalculator(), MyPropertyCalculator(my_param=5)],
init_structure=atoms,
engine=atoms.calc,
)
pipeline.run()calculator— EMT calculator (fast, no DFT needed)atoms— Al FCC bulk (a=4.05) with EMT calculator attachedelements— returns"Al"
- Forgot
param_names: If you add__init__parameters but don't list them inparam_names, serialization (todict/fromdict) will silently lose them. - Forgot
property_name: Each calculator needs a uniqueproperty_namestring. It's used as the job directory name and in pipeline JSON. - Modifying
self.basis_refdirectly: Always use.copy()when creating variants.self.basis_refis the reference structure and should not be mutated. - Returning wrong tuple from
get_structure_value: Must return(dict, Atoms). The dict goes intooutput_dict, the Atoms intooutput_structures_dict.