Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions doc/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ organisation on `GitHub <https://github.com/openbiosim/sire>`__.

* Fix recursion bug in :func:`sire.base.wrap()` function.

* Add support for passing cell vectors to ``PyQMForce`` and ``TorchQMForce``.

* Add ``--install-metadata`` option to ``setup.py`` to register development source installations
with ``conda``.

* Fix :meth:`Dynamics.get_rest2_scale()` method.

`2025.3.0 <https://github.com/openbiosim/sire/compare/2025.2.0...2025.3.0>`__ - November 2025
Expand Down
1 change: 1 addition & 0 deletions doc/source/tutorial/part08/01_intro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ signature:
charges_mm: List[float],
xyz_qm: List[List[float]],
xyz_mm: List[List[float]],
cell: Optional[List[List[float]]] = None,
idx_mm: Optional[List[int]] = None,
) -> Tuple[float, List[List[float]], List[List[float]]]:

Expand Down
94 changes: 68 additions & 26 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@
You can use `--skip-build` to skip the building of the corelib and wrappers
"""

import sys
import glob
import json
import os
import platform
import subprocess
import shutil
import glob
import sys

try:
# We have to check the version, but we can't do this by
Expand Down Expand Up @@ -282,6 +283,14 @@ def parse_args():
help="Skip the build of the C++ code (only use if you know that "
"the C++ code is already built)",
)
parser.add_argument(
"--install-metadata",
action="store_true",
default=False,
help="Install package metadata. This is useful when you are building "
"from source but still want to be able to query the installation using "
"conda list sire.",
)
parser.add_argument(
"action",
nargs="*",
Expand Down Expand Up @@ -383,8 +392,8 @@ def conda_install(
dependencies_to_skip = []

for dependency in dependencies:
# remove any quotes from the dependency
dependency = dependency.replace("\"", "")
# remove any quotes from the dependency
dependency = dependency.replace('"', "")

if dependency == "python" or is_installed(dependency, conda_exe):
# no need to install again
Expand Down Expand Up @@ -472,10 +481,8 @@ def install_requires(install_bss_reqs=False, install_emle_reqs=False, yes=True):
# this didn't import - we are missing setuptools
print("Installing setuptools")
conda_install(
["setuptools"],
install_bss_reqs,
install_emle_reqs=False,
yes=yes)
["setuptools"], install_bss_reqs, install_emle_reqs=False, yes=yes
)
try:
import pkg_resources
except Exception:
Expand Down Expand Up @@ -581,24 +588,32 @@ def build(ncores: int = 1, npycores: int = 1, coredefs=[], pydefs=[]):
if conda_build:
print("This is a conda build")

CXX = os.environ["CXX"]
CC = os.environ["CC"]

# make sure that these compilers are in the path
CXX_bin = shutil.which(CXX)
CC_bin = shutil.which(CC)

print(f"{CXX} => {CXX_bin}")
print(f"{CC} => {CC_bin}")

if CXX_bin is None or CC_bin is None:
print("Cannot find the compilers requested by conda-build in the PATH")
print("Please check that the compilers are installed and available.")
sys.exit(-1)

# use the full paths, in case CMake struggles
CXX = CXX_bin
CC = CC_bin
# Try to get compilers from environment
CXX = os.environ.get("CXX")
CC = os.environ.get("CC")

# Fallback to finding cl.exe on Windows
if (CXX is None or CC is None) and is_windows:
import shutil
cl_path = shutil.which("cl.exe") or shutil.which("cl")
if cl_path:
print(f"Compiler not in environment, using found compiler: {cl_path}")
if CXX is None:
CXX = cl_path
if CC is None:
CC = cl_path
else:
raise ValueError(
"Conda build on Windows requires CXX and CC environment variables. "
"Ensure your conda recipe includes {{ compiler('c') }} and {{ compiler('cxx') }} "
"in build requirements and that Visual Studio is properly installed."
)
elif CXX is None or CC is None:
raise ValueError(
f"Conda build detected but compiler environment variables not set. "
f"CXX={CXX}, CC={CC}. "
f"Ensure your conda recipe includes compiler requirements."
)

elif is_macos:
try:
Expand Down Expand Up @@ -920,6 +935,8 @@ def install(ncores: int = 1, npycores: int = 1):


if __name__ == "__main__":
OLDPWD = os.getcwd()

args = parse_args()

if len(args.action) != 1:
Expand Down Expand Up @@ -985,3 +1002,28 @@ def install(ncores: int = 1, npycores: int = 1):
f"Unrecognised action '{action}'. Please use 'install_requires', "
"'build', 'install' or 'install_module'"
)

# Create minimist package metadata so that 'conda list sire' works.
if args.install_metadata:
os.chdir(OLDPWD)
if "CONDA_PREFIX" in os.environ:
metadata_dir = os.path.join(os.environ["CONDA_PREFIX"], "conda-meta")
if os.path.exists(metadata_dir):
# Get the Python version.
pyver = f"py{sys.version_info.major}{sys.version_info.minor}"
metadata = {
"name": "sire",
"version": open("version.txt").readline().strip(),
"build": pyver,
"build_number": 0,
"channel": "local",
"size": 0,
"license": "GPL-3.0-or-later",
"subdir": platform_string,
}
metadata_file = os.path.join(
metadata_dir, f"sire-{metadata['version']}-{pyver}.json"
)
with open(metadata_file, "w") as f:
json.dump(metadata, f, indent=2)
print(f"Created conda package metadata file: {metadata_file}")
7 changes: 4 additions & 3 deletions src/sire/mol/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1667,9 +1667,10 @@ def _dynamics(
Whether or not to swap the end states. If this is True, then
the perturbation will run from the perturbed back to the
reference molecule (the perturbed molecule will be at lambda=0,
while the reference molecule will be at lambda=1). This will
use the coordinates of the perturbed molecule as the
starting point.
while the reference molecule will be at lambda=1). Note that this
will still use the coordinates of the reference state as the
starting point for the simulation, since it is assumed that
this reflects the current equilibrated state of the system.

ignore_perturbations: bool
Whether or not to ignore perturbations. If this is True, then
Expand Down
20 changes: 11 additions & 9 deletions tests/qm/test_qm.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def test_callback_method():
"""Makes sure that a callback method works correctly"""

class Test:
def callback(self, a, b, c, d, e=None):
def callback(self, a, b, c, d, e=None, f=None):
return (42, d, c)

# Instantiate the class.
Expand All @@ -39,19 +39,20 @@ def callback(self, a, b, c, d, e=None):
b = [3, 4]
c = [a, b]
d = [b, a]
e = [4, 5]
e = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
f = [4, 7]

# Call the callback.
result = cb.call(a, b, c, d, e)
result = cb.call(a, b, c, d, e, f)

# Make sure the result is correct.
assert result == (42, d, c) == test.callback(a, b, c, d)
assert result == (42, d, c) == test.callback(a, b, c, d, e, f)


def test_callback_function():
"""Makes sure that a callback function works correctly"""

def callback(a, b, c, d, e=None):
def callback(a, b, c, d, e=None, f=None):
return (42, d, c)

# Create a callback object.
Expand All @@ -62,13 +63,14 @@ def callback(a, b, c, d, e=None):
b = [3, 4]
c = [a, b]
d = [b, a]
e = [4, 5]
e = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
f = [4, 5]

# Call the callback.
result = cb.call(a, b, c, d, e)
result = cb.call(a, b, c, d, e, f)

# Make sure the result is correct.
assert result == (42, d, c) == callback(a, b, c, d)
assert result == (42, d, c) == callback(a, b, c, d, e, f)


@pytest.mark.parametrize(
Expand Down Expand Up @@ -419,7 +421,7 @@ def test_create_engine(ala_mols):
"""

# A test callback function. Returns a known energy and dummy forces.
def callback(numbers_qm, charges_mm, xyz_qm, xyz_mm, idx_mm=None):
def callback(numbers_qm, charges_mm, xyz_qm, xyz_mm, cell=None, idx_mm=None):
return (42, xyz_qm, xyz_mm)

# Create a local copy of the test system.
Expand Down
8 changes: 4 additions & 4 deletions wrapper/Convert/SireOpenMM/PyQMCallback.pypp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,18 @@ void register_PyQMCallback_class(){
typedef bp::class_< SireOpenMM::PyQMCallback > PyQMCallback_exposer_t;
PyQMCallback_exposer_t PyQMCallback_exposer = PyQMCallback_exposer_t( "PyQMCallback", "A callback wrapper class to interface with external QM engines\nvia the CustomCPPForceImpl.", bp::init< >("Default constructor.") );
bp::scope PyQMCallback_scope( PyQMCallback_exposer );
PyQMCallback_exposer.def( bp::init< bp::api::object, bp::optional< QString > >(( bp::arg("arg0"), bp::arg("name")="" ), "Constructor\nPar:am py_object\nA Python object that contains the callback function.\n\nPar:am name\nThe name of a callback method that take the following arguments:\n- numbers_qm: A list of atomic numbers for the atoms in the ML region.\n- charges_mm: A list of the MM charges in mod electron charge.\n- xyz_qm: A list of positions for the atoms in the ML region in Angstrom.\n- xyz_mm: A list of positions for the atoms in the MM region in Angstrom.\n- idx_mm: A list of indices for the MM atoms in the QM/MM region.\nThe callback should return a tuple containing:\n- The energy in kJmol.\n- A list of forces for the QM atoms in kJmolnm.\n- A list of forces for the MM atoms in kJmolnm.\nIf empty, then the object is assumed to be a callable.\n") );
PyQMCallback_exposer.def( bp::init< bp::api::object, bp::optional< QString > >(( bp::arg("arg0"), bp::arg("name")="" ), "Constructor\nPar:am py_object\nA Python object that contains the callback function.\n\nPar:am name\nThe name of a callback method that take the following arguments:\n- numbers_qm: A list of atomic numbers for the atoms in the ML region.\n- charges_mm: A list of the MM charges in mod electron charge.\n- xyz_qm: A list of positions for the atoms in the ML region in Angstrom.\n- xyz_mm: A list of positions for the atoms in the MM region in Angstrom.\n- cell: A list of cell vectors in Angstrom.\n- idx_mm: A list of indices for the MM atoms in the QM/MM region.\nThe callback should return a tuple containing:\n- The energy in kJmol.\n- A list of forces for the QM atoms in kJmolnm.\n- A list of forces for the MM atoms in kJmolnm.\nIf empty, then the object is assumed to be a callable.\n") );
{ //::SireOpenMM::PyQMCallback::call

typedef ::boost::tuples::tuple< double, QVector< QVector< double > >, QVector< QVector< double > >, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type > ( ::SireOpenMM::PyQMCallback::*call_function_type)( ::QVector< int >,::QVector< double >,::QVector< QVector< double > >,::QVector< QVector< double > >,::QVector< int > ) const;
typedef ::boost::tuples::tuple< double, QVector< QVector< double > >, QVector< QVector< double > >, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type > ( ::SireOpenMM::PyQMCallback::*call_function_type)( ::QVector< int >,::QVector< double >,::QVector< QVector< double > >,::QVector< QVector< double > >,::QVector<QVector< double > >, ::QVector< int > ) const;
call_function_type call_function_value( &::SireOpenMM::PyQMCallback::call );

PyQMCallback_exposer.def(
"call"
, call_function_value
, ( bp::arg("numbers_qm"), bp::arg("charges_mm"), bp::arg("xyz_qm"), bp::arg("xyz_mm"), bp::arg("idx_mm") )
, ( bp::arg("numbers_qm"), bp::arg("charges_mm"), bp::arg("xyz_qm"), bp::arg("xyz_mm"), bp::arg("cell"), bp::arg("idx_mm") )
, bp::release_gil_policy()
, "Call the callback function.\nPar:am numbers_qm\nA vector of atomic numbers for the atoms in the ML region.\n\nPar:am charges_mm\nA vector of the charges on the MM atoms in mod electron charge.\n\nPar:am xyz_qm\nA vector of positions for the atoms in the ML region in Angstrom.\n\nPar:am xyz_mm\nA vector of positions for the atoms in the MM region in Angstrom.\n\nPar:am idx_mm A vector of indices for the MM atoms in the QM/MM region. Note that len(idx_mm) <= len(charges_mm) since it only contains the indices of true MM atoms, not link atoms or virtual charges.\n\nReturn:s\nA tuple containing:\n- The energy in kJmol.\n- A vector of forces for the QM atoms in kJmolnm.\n- A vector of forces for the MM atoms in kJmolnm.\n" );
, "Call the callback function.\nPar:am numbers_qm\nA vector of atomic numbers for the atoms in the ML region.\n\nPar:am charges_mm\nA vector of the charges on the MM atoms in mod electron charge.\n\nPar:am xyz_qm\nA vector of positions for the atoms in the ML region in Angstrom.\n\nPar:am xyz_mm\nA vector of positions for the atoms in the MM region in Angstrom.\n\nPar:am cell A list of cell vectors in Angstrom.\n\nPar:am idx_mm A vector of indices for the MM atoms in the QM/MM region. Note that len(idx_mm) <= len(charges_mm) since it only contains the indices of true MM atoms, not link atoms or virtual charges.\n\nReturn:s\nA tuple containing:\n- The energy in kJmol.\n- A vector of forces for the QM atoms in kJmolnm.\n- A vector of forces for the MM atoms in kJmolnm.\n" );

}
{ //::SireOpenMM::PyQMCallback::typeName
Expand Down
6 changes: 3 additions & 3 deletions wrapper/Convert/SireOpenMM/PyQMEngine.pypp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,15 @@ void register_PyQMEngine_class(){
PyQMEngine_exposer.def( bp::init< SireOpenMM::PyQMEngine const & >(( bp::arg("other") ), "Copy constructor.") );
{ //::SireOpenMM::PyQMEngine::call

typedef ::boost::tuples::tuple< double, QVector< QVector< double > >, QVector< QVector< double > >, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type > ( ::SireOpenMM::PyQMEngine::*call_function_type)( ::QVector< int >,::QVector< double >,::QVector< QVector< double > >,::QVector< QVector< double > >,::QVector < int > ) const;
typedef ::boost::tuples::tuple< double, QVector< QVector< double > >, QVector< QVector< double > >, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type > ( ::SireOpenMM::PyQMEngine::*call_function_type)( ::QVector< int >,::QVector< double >,::QVector< QVector< double > >,::QVector< QVector< double > >,::QVector< QVector< double > >,::QVector < int > ) const;
call_function_type call_function_value( &::SireOpenMM::PyQMEngine::call );

PyQMEngine_exposer.def(
"call"
, call_function_value
, ( bp::arg("numbers_qm"), bp::arg("charges_mm"), bp::arg("xyz_qm"), bp::arg("xyz_mm"), bp::arg("idx_mm") )
, ( bp::arg("numbers_qm"), bp::arg("charges_mm"), bp::arg("xyz_qm"), bp::arg("xyz_mm"), bp::arg("cell"), bp::arg("idx_mm") )
, bp::release_gil_policy()
, "Call the callback function.\nPar:am numbers_qm\nA vector of atomic numbers for the atoms in the ML region.\n\nPar:am charges_mm\nA vector of the charges on the MM atoms in mod electron charge.\n\nPar:am xyz_qm\nA vector of positions for the atoms in the ML region in Angstrom.\n\nPar:am xyz_mm\nA vector of positions for the atoms in the MM region in Angstrom.\n\nPar:am idx_mm A vector of indices for the MM atoms in the QM/MM region. Note that len(idx_mm) <= len(charges_mm) since it only contains the indices of the true MM atoms, not link atoms or virtual charges.\n\nReturn:s\nA tuple containing:\n- The energy in kJmol.\n- A vector of forces for the QM atoms in kJmolnm.\n- A vector of forces for the MM atoms in kJmolnm.\n" );
, "Call the callback function.\nPar:am numbers_qm\nA vector of atomic numbers for the atoms in the ML region.\n\nPar:am charges_mm\nA vector of the charges on the MM atoms in mod electron charge.\n\nPar:am xyz_qm\nA vector of positions for the atoms in the ML region in Angstrom.\n\nPar:am xyz_mm\nA vector of positions for the atoms in the MM region in Angstrom.\n\nPar:am cell A list of cell vectors in Angstrom.\n\nPar:am idx_mm A vector of indices for the MM atoms in the QM/MM region. Note that len(idx_mm) <= len(charges_mm) since it only contains the indices of the true MM atoms, not link atoms or virtual charges.\n\nReturn:s\nA tuple containing:\n- The energy in kJmol.\n- A vector of forces for the QM atoms in kJmolnm.\n- A vector of forces for the MM atoms in kJmolnm.\n" );

}
{ //::SireOpenMM::PyQMEngine::getAtoms
Expand Down
Loading
Loading