diff --git a/.ci/pytorch/requirements.txt b/.ci/pytorch/requirements.txt new file mode 100644 index 0000000000..d5a36f9713 --- /dev/null +++ b/.ci/pytorch/requirements.txt @@ -0,0 +1,5 @@ +# The extra url makes pip grap the torch fersion we want to use in the regtests +--extra-index-url=https://download.pytorch.org/whl/cpu +torch>=2.7 +metatomic-torch>=0.1.3,<0.2 +featomic-torch==0.7.0 diff --git a/.github/workflows/linuxWF.yml b/.github/workflows/linuxWF.yml index 02bfecf877..e59b9fbb8f 100644 --- a/.github/workflows/linuxWF.yml +++ b/.github/workflows/linuxWF.yml @@ -47,7 +47,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: "3.10" - name: Set paths run: | echo "$HOME/opt/bin" >> $GITHUB_PATH @@ -77,17 +77,15 @@ jobs: - name: Install python packages needed for tests run: | python -m pip install --upgrade pip - pip install --user --extra-index-url=https://download.pytorch.org/whl/cpu \ + pip install --user \ Cython \ numpy \ pytest \ six \ pandas \ mdtraj \ - MDAnalysis \ - "torch>=2.7" \ - "metatomic-torch>=0.1.3,<0.2" \ - "featomic-torch==0.7.0" + MDAnalysis + pip install --user -r ./.ci/pytorch/requirements.txt # torch 2.7 above is the first one to use cxx11 ABI for the PyPI wheels diff --git a/docker/fedora39-pycv b/docker/fedora39-pycv index d5cd7e0693..e2a0fb36ba 100644 --- a/docker/fedora39-pycv +++ b/docker/fedora39-pycv @@ -17,7 +17,4 @@ RUN source /etc/bashrc \ && cd plugins/pycv \ && ./configurePyCV.sh \ && ln -s $(realpath ../../regtest/scripts) ./regtest/scripts \ - && make check_standalone -# To whom is reading: python > 3.10 gives some problems when using pycv -# from plumed whithin python. SO please remember to chante "check_standalone" -#into "check" to verify that the problem is fixed + && make check diff --git a/plugins/pycv/FindPlumed.cmake b/plugins/pycv/FindPlumed.cmake index 19315dbd66..cb218f0d5e 100644 --- a/plugins/pycv/FindPlumed.cmake +++ b/plugins/pycv/FindPlumed.cmake @@ -66,10 +66,10 @@ if(NOT Plumed_FOUND) endif() endforeach() endif() - if(${_line} MATCHES ".*-fopenmp.*") - set(Plumed_HAS_MPI + if(${_line} MATCHES ".*-[fq]openmp.*") + set(Plumed_HAS_OPENMP 1 - CACHE INTERNAL "plumed has MPI") + CACHE INTERNAL "plumed has OpenMP") endif() endforeach() diff --git a/plugins/pycv/src/PythonCVInterface.cpp b/plugins/pycv/src/PythonCVInterface.cpp index 9d41ac5eab..e984d9f6ff 100644 --- a/plugins/pycv/src/PythonCVInterface.cpp +++ b/plugins/pycv/src/PythonCVInterface.cpp @@ -418,187 +418,191 @@ void PythonCVInterface::registerKeywords( Keywords& keys ) { //TODO: add callable checks!!! -PythonCVInterface::PythonCVInterface(const ActionOptions&ao) ://the catch only applies to pybind11 things +PythonCVInterface::PythonCVInterface(const ActionOptions&ao) try ://the catch only applies to pybind11 things PLUMED_COLVAR_INIT(ao), - ActionWithPython(ao) { - try { - py::gil_scoped_acquire gil; - //Loading the python module - std::string import; - parse("IMPORT",import); - //setting up the calculate function - std::string calculateFunName; - parse("CALCULATE",calculateFunName); - log.printf(" will import %s and call function %s\n", import.c_str(), - calculateFunName.c_str()); - // Initialize the module and function pointers - pyModule = py::module::import(import.c_str()); - if (!py::hasattr(pyModule,calculateFunName.c_str())) { - error("the function " + calculateFunName + " is not present in "+ import); - } + ActionWithPython(ao), + dataContainer([] { + //We need the gil to initialize python objects + py::gil_scoped_acquire gil; + return py::dict(); +}()) { + py::gil_scoped_acquire gil; + //Loading the python module + std::string import; + parse("IMPORT",import); + //setting up the calculate function + std::string calculateFunName; + parse("CALCULATE",calculateFunName); + log.printf(" will import %s and call function %s\n", import.c_str(), + calculateFunName.c_str()); + // Initialize the module and function pointers + pyModule = py::module::import(import.c_str()); + if (!py::hasattr(pyModule,calculateFunName.c_str())) { + error("the function " + calculateFunName + " is not present in "+ import); + } - pyCalculate = pyModule.attr(calculateFunName.c_str()); - std::string initFunName; - parse("INIT",initFunName); - py::dict initDict; - if(py::hasattr(pyModule,initFunName.c_str())) { - log.printf(" will use %s during the initialization\n", initFunName.c_str()); - auto initFcn = pyModule.attr(initFunName.c_str()); - if (py::isinstance(initFcn)) { - initDict = initFcn; - } else { - initDict = initFcn(this); - } - } else if(initFunName!=PYCV_DEFAULTINIT) { - //If the default INIT is not preset, is not a problem - error("the function "+ initFunName + " is not present in "+ import); + pyCalculate = pyModule.attr(calculateFunName.c_str()); + std::string initFunName; + parse("INIT",initFunName); + py::dict initDict; + if(py::hasattr(pyModule,initFunName.c_str())) { + log.printf(" will use %s during the initialization\n", initFunName.c_str()); + auto initFcn = pyModule.attr(initFunName.c_str()); + if (py::isinstance(initFcn)) { + initDict = initFcn; + } else { + initDict = initFcn(this); } + } else if(initFunName!=PYCV_DEFAULTINIT) { + //If the default INIT is not preset, is not a problem + error("the function "+ initFunName + " is not present in "+ import); + } - std::string prepareFunName; - parse("PREPARE",prepareFunName); - if (prepareFunName!=PYCV_NOTIMPLEMENTED) { - if (!py::hasattr(pyModule,prepareFunName.c_str())) { - error("the function " + prepareFunName + " is not present in "+ import); - } - hasPrepare=true; - pyPrepare=pyModule.attr(prepareFunName.c_str()); - log.printf(" will use %s while calling prepare() before calculate()\n", prepareFunName.c_str()); + std::string prepareFunName; + parse("PREPARE",prepareFunName); + if (prepareFunName!=PYCV_NOTIMPLEMENTED) { + if (!py::hasattr(pyModule,prepareFunName.c_str())) { + error("the function " + prepareFunName + " is not present in "+ import); } + hasPrepare=true; + pyPrepare=pyModule.attr(prepareFunName.c_str()); + log.printf(" will use %s while calling prepare() before calculate()\n", prepareFunName.c_str()); + } - std::string updateFunName; - parse("UPDATE",updateFunName); - if (updateFunName!=PYCV_NOTIMPLEMENTED) { - if (!py::hasattr(pyModule,updateFunName.c_str())) { - error("the function " + updateFunName + " is not present in " + import); - } - pyUpdate=pyModule.attr(updateFunName.c_str()); - hasUpdate=true; - log.printf(" will use %s while calling update() after calculate()\n", updateFunName.c_str()); + std::string updateFunName; + parse("UPDATE",updateFunName); + if (updateFunName!=PYCV_NOTIMPLEMENTED) { + if (!py::hasattr(pyModule,updateFunName.c_str())) { + error("the function " + updateFunName + " is not present in " + import); } + pyUpdate=pyModule.attr(updateFunName.c_str()); + hasUpdate=true; + log.printf(" will use %s while calling update() after calculate()\n", updateFunName.c_str()); + } - { - std::vector components; - parseVector("COMPONENTS", components); - if (components.size()>1) { - error("Please define multiple COMPONENTS from INIT in python."); - } + { + std::vector components; + parseVector("COMPONENTS", components); + if (components.size()>1) { + error("Please define multiple COMPONENTS from INIT in python."); } + } - if(initDict.contains("COMPONENTS")) { - if(initDict.contains("Value")) { - error("The initialize dict cannot contain both \"Value\" and \"COMPONENTS\""); - } - if(!py::isinstance(initDict["COMPONENTS"])) { - error("COMPONENTS must be a dictionary using with the name of the components as keys"); - } - py::dict components=initDict["COMPONENTS"]; - for(auto comp: components) { - auto settings = py::cast(comp.second); - if(components.size()==1) { //a single component - initializeValue(dynamic_cast<::PLMD::ActionWithValue&>(*this), settings); - valueSettings(settings,getPntrToValue()); - } else { - auto name=std::string(PYCV_COMPONENTPREFIX) - +"-"+py::cast(comp.first); - initializeComponent(dynamic_cast<::PLMD::ActionWithValue&>(*this), - name, - settings); - valueSettings(settings,getPntrToComponent(name)); - } + if(initDict.contains("COMPONENTS")) { + if(initDict.contains("Value")) { + error("The initialize dict cannot contain both \"Value\" and \"COMPONENTS\""); + } + if(!py::isinstance(initDict["COMPONENTS"])) { + error("COMPONENTS must be a dictionary using with the name of the components as keys"); + } + py::dict components=initDict["COMPONENTS"]; + for(auto comp: components) { + auto settings = py::cast(comp.second); + if(components.size()==1) { //a single component + initializeValue(dynamic_cast<::PLMD::ActionWithValue&>(*this), settings); + valueSettings(settings,getPntrToValue()); + } else { + auto name=std::string(PYCV_COMPONENTPREFIX) + +"-"+py::cast(comp.first); + initializeComponent(dynamic_cast<::PLMD::ActionWithValue&>(*this), + name, + settings); + valueSettings(settings,getPntrToComponent(name)); } - - } else if(initDict.contains("Value")) { - py::dict settingsDict=initDict["Value"]; - initializeValue(dynamic_cast<::PLMD::ActionWithValue&>(*this),settingsDict); - valueSettings(settingsDict,getPntrToValue()); - } else { - warning(" WARNING: by defaults components periodicity is not set and component is added without derivatives - see manual\n"); - //this will crash with an error, beacuse periodicity is not explicitly set - addValue(); } - std::vector atoms; - pyParseAtomList("ATOMS",initDict,atoms); - std::vector groupA; - pyParseAtomList("GROUPA",initDict,groupA); - std::vector groupB; - pyParseAtomList("GROUPB",initDict,groupB); + } else if(initDict.contains("Value")) { + py::dict settingsDict=initDict["Value"]; + initializeValue(dynamic_cast<::PLMD::ActionWithValue&>(*this),settingsDict); + valueSettings(settingsDict,getPntrToValue()); + } else { + warning(" WARNING: by defaults components periodicity is not set and component is added without derivatives - see manual\n"); + //this will crash with an error, beacuse periodicity is not explicitly set + addValue(); + } - if(atoms.size() !=0 && groupA.size()!=0) { - error("you can choose only between using the neigbourlist OR the atoms"); - } + std::vector atoms; + pyParseAtomList("ATOMS",initDict,atoms); + std::vector groupA; + pyParseAtomList("GROUPA",initDict,groupA); + std::vector groupB; + pyParseAtomList("GROUPB",initDict,groupB); - if(atoms.size()==0&& groupA.size()==0 && groupB.size()==0) { - error("At least one atom is required"); - } + if(atoms.size() !=0 && groupA.size()!=0) { + error("you can choose only between using the neigbourlist OR the atoms"); + } - if (atoms.size() != 0 && groupA.size() != 0) { - error("you can choose only between using the neigbourlist OR the atoms"); - } + if(atoms.size()==0&& groupA.size()==0 && groupB.size()==0) { + error("At least one atom is required"); + } - if (atoms.size() == 0 && groupA.size() == 0 && groupB.size() == 0) { - error("At least one atom is required"); - } + if (atoms.size() != 0 && groupA.size() != 0) { + error("you can choose only between using the neigbourlist OR the atoms"); + } - bool nopbc; - pyParseFlag("NOPBC",initDict, nopbc); - pbc = !nopbc; - - if (groupA.size() > 0) { - // parse the NL things only in the NL case - bool dopair; - pyParseFlag("PAIR",initDict, dopair); - // this is a WIP - bool serial = false; - bool doneigh; - pyParseFlag("NLIST",initDict,doneigh); - double nl_cut = 0.0; - int nl_st = 0; - if (doneigh) { - pyParse("NL_CUTOFF", initDict, nl_cut); - if (nl_cut <= 0.0) { - error("NL_CUTOFF should be explicitly specified and positive"); - } - pyParse("NL_STRIDE",initDict, nl_st); - if (nl_st <= 0) { - error("NL_STRIDE should be explicitly specified and positive"); - } + if (atoms.size() == 0 && groupA.size() == 0 && groupB.size() == 0) { + error("At least one atom is required"); + } + + bool nopbc; + pyParseFlag("NOPBC",initDict, nopbc); + pbc = !nopbc; + + if (groupA.size() > 0) { + // parse the NL things only in the NL case + bool dopair; + pyParseFlag("PAIR",initDict, dopair); + // this is a WIP + bool serial = false; + bool doneigh; + pyParseFlag("NLIST",initDict,doneigh); + double nl_cut = 0.0; + int nl_st = 0; + if (doneigh) { + pyParse("NL_CUTOFF", initDict, nl_cut); + if (nl_cut <= 0.0) { + error("NL_CUTOFF should be explicitly specified and positive"); } - // endof WIP - if (groupB.size() > 0) { - if (doneigh) - nl = Tools::make_unique( - groupA, groupB, serial, dopair, pbc, getPbc(), comm, nl_cut, nl_st); - else - nl = Tools::make_unique(groupA, groupB, serial, dopair, - pbc, getPbc(), comm); - } else { - if (doneigh) - nl = Tools::make_unique(groupA, serial, pbc, getPbc(), - comm, nl_cut, nl_st); - else - nl = Tools::make_unique(groupA, serial, pbc, getPbc(), - comm); + pyParse("NL_STRIDE",initDict, nl_st); + if (nl_st <= 0) { + error("NL_STRIDE should be explicitly specified and positive"); } - requestAtoms(nl->getFullAtomList()); - } else { - requestAtoms(atoms); } - - if (getNumberOfComponents()>1) { - log.printf(" it is expected to return dictionaries with %d components\n", - getNumberOfComponents()); + // endof WIP + if (groupB.size() > 0) { + if (doneigh) + nl = Tools::make_unique( + groupA, groupB, serial, dopair, pbc, getPbc(), comm, nl_cut, nl_st); + else + nl = Tools::make_unique(groupA, groupB, serial, dopair, + pbc, getPbc(), comm); + } else { + if (doneigh) + nl = Tools::make_unique(groupA, serial, pbc, getPbc(), + comm, nl_cut, nl_st); + else + nl = Tools::make_unique(groupA, serial, pbc, getPbc(), + comm); } + requestAtoms(nl->getFullAtomList()); + } else { + requestAtoms(atoms); + } - log << " Bibliography " << plumed.cite(PYTHONCV_CITATION) << "\n"; - // NB: the NL kewywords will be counted as error when using ATOMS - checkRead(); - } catch (const py::error_already_set &e) { - error(e.what()); - //vdbg(e.what()); + if (getNumberOfComponents()>1) { + log.printf(" it is expected to return dictionaries with %d components\n", + getNumberOfComponents()); } + + log << " Bibliography " << plumed.cite(PYTHONCV_CITATION) << "\n"; + // NB: the NL kewywords will be counted as error when using ATOMS + checkRead(); +} catch (const py::error_already_set &e) { + error(e.what()); + //vdbg(e.what()); } + void PythonCVInterface::prepare() { try { if (nl) { diff --git a/plugins/pycv/src/PythonCVInterface.h b/plugins/pycv/src/PythonCVInterface.h index 23bd60f69f..bf21cae4dd 100644 --- a/plugins/pycv/src/PythonCVInterface.h +++ b/plugins/pycv/src/PythonCVInterface.h @@ -49,7 +49,7 @@ class PythonCVInterface : public Colvar, public ActionWithPython { void calculateMultiComponent(pybind11::object &); void readReturn(const pybind11::object &, Value* ); public: - ::pybind11::dict dataContainer {}; + ::pybind11::dict dataContainer; explicit PythonCVInterface(const ActionOptions&); static void registerKeywords( Keywords& keys ); // active methods: diff --git a/python/test/test_input_builder.py b/python/test/test_input_builder.py index 5c4d6dcabc..84f3819d88 100644 --- a/python/test/test_input_builder.py +++ b/python/test/test_input_builder.py @@ -1,178 +1,237 @@ - import unittest import plumed -ib=plumed.InputBuilder() +try: + import MDAnalysis + + _HAS_MDANALYSIS = True +except ModuleNotFoundError: + _HAS_MDANALYSIS = False try: - import MDAnalysis - _HAS_MDANALYSIS=True -except: - _HAS_MDANALYSIS=False - -class Test(unittest.TestCase): - - def check(self,s1,s2): - self.assertEqual(s1,s2) - - def checkfiles(self,f1,f2): - import filecmp - import difflib - import sys - if not filecmp.cmp(f1,f2): - s1=[] - with open(f1,"r") as file1: - for l in file1: - s1.append(l) - s2=[] - with open(f2,"r") as file2: - for l in file2: - s2.append(l) - for line in difflib.context_diff(s1, s2, fromfile=f1, tofile=f2): - sys.stdout.write(line) - self.assertTrue(False) - - def test1(self): - self.check(ib.TORSION("phi",ATOMS="5,7,9,15") , 'phi: TORSION ATOMS=5,7,9,15\n') - - def test1b(self): - self.check(ib.TORSION__("phi",ATOMS="5,7,9,15") , 'phi: TORSION ATOMS=5,7,9,15\n') - - def test2(self): - self.check(ib.TORSION("phi",ATOMS="5 7 9 15") , 'phi: TORSION ATOMS={5 7 9 15}\n') - - def test3(self): - self.check(ib.COORDINATION(GROUPA="1-10",GROUPB="11-20",SWITCH="RATIONAL NN=6 R_0=1") , 'COORDINATION GROUPA=1-10 GROUPB=11-20 SWITCH={RATIONAL NN=6 R_0=1}\n') - - def test4(self): - self.check(ib.COORDINATION(GROUPA=""" + import numpy + + _HAS_NUMPY = True +except ModuleNotFoundError: + _HAS_NUMPY = False + +# Without the declaration of a plumed object some environment +# configuration tend to raise segmentation faults after the +# kernel is finalized while allocation the InputBuilder. +p = plumed.Plumed() +ib = plumed.InputBuilder() + + +class TestInputBuilder(unittest.TestCase): + def check(self, s1, s2): + self.assertEqual(s1, s2) + + def test_cv_label(self): + self.check(ib.TORSION("phi", ATOMS="5,7,9,15"), "phi: TORSION ATOMS=5,7,9,15\n") + + def test_cv_special_access(self): + self.check( + ib.TORSION__("phi", ATOMS="5,7,9,15"), "phi: TORSION ATOMS=5,7,9,15\n" + ) + + def test_cv_space_separed_arguments(self): + self.check( + ib.TORSION("phi", ATOMS="5 7 9 15"), "phi: TORSION ATOMS={5 7 9 15}\n" + ) + + def test_cv_subkeyword(self): + self.check( + ib.COORDINATION( + GROUPA="1-10", GROUPB="11-20", SWITCH="RATIONAL NN=6 R_0=1" + ), + "COORDINATION GROUPA=1-10 GROUPB=11-20 SWITCH={RATIONAL NN=6 R_0=1}\n", + ) + + def test_cv_triplequotestring(self): + self.check( + ib.COORDINATION( + GROUPA=""" 1 2 3 4 5 6 7 8 9 10 - """,GROUPB=""" + """, + GROUPB=""" 11 12 13 14 15 16 17 18 19 20 - """,SWITCH=""" + """, + SWITCH=""" RATIONAL NN=6 R_0=1 - """) , 'COORDINATION GROUPA={ 1 2 3 4 5 6 7 8 9 10 } GROUPB={ 11 12 13 14 15 16 17 18 19 20 } SWITCH={ RATIONAL NN=6 R_0=1 }\n') - - def test5(self): - self.check(ib.DISTANCE("d",ATOMS="11 21",NOPBC=True) , 'd: DISTANCE ATOMS={11 21} NOPBC\n') - - def test6(self): - self.check(ib.DISTANCE("d",ATOMS="11 21",NOPBC=False),'d: DISTANCE ATOMS={11 21}\n') - - def test7(self): - self.check(ib.METAD(ARG="phi psi",PACE=500,HEIGHT=1.2,SIGMA="0.35 0.35",FILE="HILLS"),'METAD ARG={phi psi} FILE=HILLS HEIGHT=1.2 PACE=500 SIGMA={0.35 0.35}\n') - - def test8(self): - self.check(ib.GROUP("g",ATOMS=range(1,101)),'g: GROUP ATOMS={1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100}\n') - - def test9(self): - self.check(ib.METAD(ARG=("phi","psi"),PACE=500,HEIGHT=1.2,SIGMA=(0.35,"pi/8"),FILE="HILLS"),'METAD ARG={phi psi} FILE=HILLS HEIGHT=1.2 PACE=500 SIGMA={0.35 pi/8}\n') - - def test10(self): - self.check(ib.MOVINGRESTRAINT(ARG="d1",KAPPA0=0,KAPPA1=10.0,AT0=20,AT1=20,STEP0=1,STEP1=10000),'MOVINGRESTRAINT ARG=d1 AT0=20 AT1=20 KAPPA0=0 KAPPA1=10.0 STEP0=1 STEP1=10000\n') - - def test11(self): - self.check( - ib.MOVINGRESTRAINT(ARG="d1",KAPPA=ib.numbered([0,10.0]),AT=ib.numbered([20,20]),STEP=ib.numbered([1,10000])) - , - 'MOVINGRESTRAINT ARG=d1 AT0=20 AT1=20 KAPPA0=0 KAPPA1=10.0 STEP0=1 STEP1=10000\n' - ) - - def test12(self): - self.check( - ib.MOVINGRESTRAINT(ARG="d1",KAPPA=ib.numbered([100]),AT=ib.numbered({0:0.0,2:10.0}),STEP=ib.numbered((0,10,20,30))) - , - 'MOVINGRESTRAINT ARG=d1 AT0=0.0 AT2=10.0 KAPPA0=100 STEP0=0 STEP1=10 STEP2=20 STEP3=30\n' - ) - - def test13(self): - self.check( - ib.MOVINGRESTRAINT(ARG="d1,d2", - KAPPA=ib.numbered([11]), - AT=ib.numbered(((0.0,1.0),(2.0,3.0))), - STEP=ib.numbered((0,100))) - , - 'MOVINGRESTRAINT ARG=d1,d2 AT0={0.0 1.0} AT1={2.0 3.0} KAPPA0=11 STEP0=0 STEP1=100\n' - ) - - def test14(self): - self.check( - ib.MOVINGRESTRAINT(ARG="d1,d2", - KAPPA=ib.numbered([100]),AT=ib.numbered(([0.0,"pi"],[2.0,"pi"])), - STEP=ib.numbered((0,100))) - , - 'MOVINGRESTRAINT ARG=d1,d2 AT0={0.0 pi} AT1={2.0 pi} KAPPA0=100 STEP0=0 STEP1=100\n' - ) - - def test15(self): - self.check( - ib.RESTRAINT(ARG="d1",KAPPA=10,AT=ib.replicas((0.0,1.0,2.0,3.0))) - , - 'RESTRAINT ARG=d1 AT=@replicas:{0.0 1.0 2.0 3.0} KAPPA=10\n' - ) - - def test16(self): - try: - import numpy - except: - print("This test requires numpy module installed.") - self.check( - ib.RESTRAINT(ARG="d1",KAPPA=10,AT=ib.replicas(numpy.linspace(3.0,5.0,17))) - , - 'RESTRAINT ARG=d1 AT=@replicas:{3.0 3.125 3.25 3.375 3.5 3.625 3.75 3.875 4.0 4.125 4.25 4.375 4.5 4.625 4.75 4.875 5.0} KAPPA=10\n' - ) - - def test17(self): - self.check( - ib.RESTRAINT(ARG="d1,d2",KAPPA=(10,10),AT=ib.replicas(([0.0,1.0],[10.0,11.0]))) - , - 'RESTRAINT ARG=d1,d2 AT=@replicas:{{0.0 1.0} {10.0 11.0}} KAPPA={10 10}\n' - ) - - def test18(self): - ib1=plumed.InputBuilder(comma_separator=True) - self.check( - ib1.GROUP("g1",ATOMS=[1,2,3,4,5,6,7,8,9,10]) - , - 'g1: GROUP ATOMS=1,2,3,4,5,6,7,8,9,10\n' - ) - - def test19(self): - self.check(ib.at.phi(6),'@phi-6') - - def test20(self): - self.check(ib.at.phi(range(1,11),"A"),'@phi-A1 @phi-A2 @phi-A3 @phi-A4 @phi-A5 @phi-A6 @phi-A7 @phi-A8 @phi-A9 @phi-A10') - - def test20(self): - self.check(ib.at.phi(range(1,11),["A","B"]),'@phi-A1 @phi-A2 @phi-A3 @phi-A4 @phi-A5 @phi-A6 @phi-A7 @phi-A8 @phi-A9 @phi-A10 @phi-B1 @phi-B2 @phi-B3 @phi-B4 @phi-B5 @phi-B6 @phi-B7 @phi-B8 @phi-B9 @phi-B10') - - def test21(self): - self.check(ib.at.phi(4,[1,2]),'@phi-1_4 @phi-2_4') - - def test22(self): - self.check(ib.at("OW",range(20,40)),'@OW-20 @OW-21 @OW-22 @OW-23 @OW-24 @OW-25 @OW-26 @OW-27 @OW-28 @OW-29 @OW-30 @OW-31 @OW-32 @OW-33 @OW-34 @OW-35 @OW-36 @OW-37 @OW-38 @OW-39') - - def test23(self): - self.check(ib.at.mdatoms,'@mdatoms') - - def test24(self): - self.check(ib.RESTRAINT(ARG="d1,d2",verbatim="AT={10 20} KAPPA={5 6}"),'RESTRAINT ARG=d1,d2 AT={10 20} KAPPA={5 6}\n') - - def test25(self): - self.check(ib.verbatim("# here is a comment"),'# here is a comment\n') - - if _HAS_MDANALYSIS: - def test_mdanalysis(self): - u=MDAnalysis.Universe("test/ref.pdb") - self.check( - ib.GROUP(ATOMS=u.select_atoms("name C2 C4 C6")), - 'GROUP ATOMS={16 20 25 50 54 59 84 88 93 118 122 127 147 151 156 178 182 187 209 213 218 240 244 249}\n' - ) - self.check( - ib.GROUP(ATOMS=u.select_atoms("name C2","name C4","name C6")), - 'GROUP ATOMS={20 54 88 122 156 187 218 249 25 59 93 127 151 182 213 244 16 50 84 118 147 178 209 240}\n' + """, + ), + "COORDINATION GROUPA={ 1 2 3 4 5 6 7 8 9 10 } GROUPB={ 11 12 13 14 15 16 17 18 19 20 } SWITCH={ RATIONAL NN=6 R_0=1 }\n", + ) + + def test_cv_flag_on(self): + self.check( + ib.DISTANCE("d", ATOMS="11 21", NOPBC=True), + "d: DISTANCE ATOMS={11 21} NOPBC\n", + ) + + def test_cv_flag_off(self): + self.check( + ib.DISTANCE("d", ATOMS="11 21", NOPBC=False), "d: DISTANCE ATOMS={11 21}\n" + ) + + def test_cv_args_integer_float(self): + self.check( + ib.METAD( + ARG="phi psi", PACE=500, HEIGHT=1.2, SIGMA="0.35 0.35", FILE="HILLS" + ), + "METAD ARG={phi psi} FILE=HILLS HEIGHT=1.2 PACE=500 SIGMA={0.35 0.35}\n", + ) + + def test_group_range(self): + self.check( + ib.GROUP("g", ATOMS=range(1, 101)), + "g: GROUP ATOMS={1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100}\n", + ) + + def test_cv_tuple_to_array(self): + self.check( + ib.METAD( + ARG=("phi", "psi"), + PACE=500, + HEIGHT=1.2, + SIGMA=(0.35, "pi/8"), + FILE="HILLS", + ), + "METAD ARG={phi psi} FILE=HILLS HEIGHT=1.2 PACE=500 SIGMA={0.35 pi/8}\n", + ) + + def test_cv_numbered_explicit(self): + self.check( + ib.MOVINGRESTRAINT( + ARG="d1", KAPPA0=0, KAPPA1=10.0, AT0=20, AT1=20, STEP0=1, STEP1=10000 + ), + "MOVINGRESTRAINT ARG=d1 AT0=20 AT1=20 KAPPA0=0 KAPPA1=10.0 STEP0=1 STEP1=10000\n", + ) + + def test_numbered(self): + self.check( + ib.MOVINGRESTRAINT( + ARG="d1", + KAPPA=ib.numbered([0, 10.0]), + AT=ib.numbered([20, 20]), + STEP=ib.numbered([1, 10000]), + ), + "MOVINGRESTRAINT ARG=d1 AT0=20 AT1=20 KAPPA0=0 KAPPA1=10.0 STEP0=1 STEP1=10000\n", + ) + + def test_numbered_set_tuple(self): + self.check( + ib.MOVINGRESTRAINT( + ARG="d1", + KAPPA=ib.numbered([100]), + AT=ib.numbered({0: 0.0, 2: 10.0}), + STEP=ib.numbered((0, 10, 20, 30)), + ), + "MOVINGRESTRAINT ARG=d1 AT0=0.0 AT2=10.0 KAPPA0=100 STEP0=0 STEP1=10 STEP2=20 STEP3=30\n", + ) + + def test_numbered_subset(self): + self.check( + ib.MOVINGRESTRAINT( + ARG="d1,d2", + KAPPA=ib.numbered([11]), + AT=ib.numbered(((0.0, 1.0), (2.0, 3.0))), + STEP=ib.numbered((0, 100)), + ), + "MOVINGRESTRAINT ARG=d1,d2 AT0={0.0 1.0} AT1={2.0 3.0} KAPPA0=11 STEP0=0 STEP1=100\n", + ) + + def test_numbered_pi(self): + self.check( + ib.MOVINGRESTRAINT( + ARG="d1,d2", + KAPPA=ib.numbered([100]), + AT=ib.numbered(([0.0, "pi"], [2.0, "pi"])), + STEP=ib.numbered((0, 100)), + ), + "MOVINGRESTRAINT ARG=d1,d2 AT0={0.0 pi} AT1={2.0 pi} KAPPA0=100 STEP0=0 STEP1=100\n", + ) + + def test_replicas(self): + self.check( + ib.RESTRAINT(ARG="d1", KAPPA=10, AT=ib.replicas((0.0, 1.0, 2.0, 3.0))), + "RESTRAINT ARG=d1 AT=@replicas:{0.0 1.0 2.0 3.0} KAPPA=10\n", + ) + + @unittest.skipIf(not _HAS_NUMPY, "Test runnable only if numpy is installed") + def test_numpy_input(self): + self.check( + ib.RESTRAINT( + ARG="d1", KAPPA=10, AT=ib.replicas(numpy.linspace(3.0, 5.0, 17)) + ), + "RESTRAINT ARG=d1 AT=@replicas:{3.0 3.125 3.25 3.375 3.5 3.625 3.75 3.875 4.0 4.125 4.25 4.375 4.5 4.625 4.75 4.875 5.0} KAPPA=10\n", + ) + + def test_replicas_array(self): + self.check( + ib.RESTRAINT( + ARG="d1,d2", KAPPA=(10, 10), AT=ib.replicas(([0.0, 1.0], [10.0, 11.0])) + ), + "RESTRAINT ARG=d1,d2 AT=@replicas:{{0.0 1.0} {10.0 11.0}} KAPPA={10 10}\n", + ) + + def test_comma_separator(self): + ib1 = plumed.InputBuilder(comma_separator=True) + self.check( + ib1.GROUP("g1", ATOMS=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + "g1: GROUP ATOMS=1,2,3,4,5,6,7,8,9,10\n", + ) + + def test_at_phi(self): + self.check(ib.at.phi(6), "@phi-6") + + def test_at_phi_range(self): + self.check( + ib.at.phi(range(1, 11), "A"), + "@phi-A1 @phi-A2 @phi-A3 @phi-A4 @phi-A5 @phi-A6 @phi-A7 @phi-A8 @phi-A9 @phi-A10", + ) + + def test_at_phi_range_array(self): + self.check( + ib.at.phi(range(1, 11), ["A", "B"]), + "@phi-A1 @phi-A2 @phi-A3 @phi-A4 @phi-A5 @phi-A6 @phi-A7 @phi-A8 @phi-A9 @phi-A10 @phi-B1 @phi-B2 @phi-B3 @phi-B4 @phi-B5 @phi-B6 @phi-B7 @phi-B8 @phi-B9 @phi-B10", + ) + + def test_at_phi_array(self): + self.check(ib.at.phi(4, [1, 2]), "@phi-1_4 @phi-2_4") + + def test_at_range(self): + self.check( + ib.at("OW", range(20, 40)), + "@OW-20 @OW-21 @OW-22 @OW-23 @OW-24 @OW-25 @OW-26 @OW-27 @OW-28 @OW-29 @OW-30 @OW-31 @OW-32 @OW-33 @OW-34 @OW-35 @OW-36 @OW-37 @OW-38 @OW-39", + ) + + def test_at_mdatoms(self): + self.check(ib.at.mdatoms, "@mdatoms") + + def test_verbatim_cv(self): + self.check( + ib.RESTRAINT(ARG="d1,d2", verbatim="AT={10 20} KAPPA={5 6}"), + "RESTRAINT ARG=d1,d2 AT={10 20} KAPPA={5 6}\n", + ) + + def test_verbatim_comment(self): + self.check(ib.verbatim("# here is a comment"), "# here is a comment\n") + + @unittest.skipIf( + not _HAS_MDANALYSIS, "Test runnable only if MDAnalysis is installed" ) + def test_mdanalysis(self): + u = MDAnalysis.Universe("test/ref.pdb") + self.check( + ib.GROUP(ATOMS=u.select_atoms("name C2 C4 C6")), + "GROUP ATOMS={16 20 25 50 54 59 84 88 93 118 122 127 147 151 156 178 182 187 209 213 218 240 244 249}\n", + ) + self.check( + ib.GROUP(ATOMS=u.select_atoms("name C2", "name C4", "name C6")), + "GROUP ATOMS={20 54 88 122 156 187 218 249 25 59 93 127 151 182 213 244 16 50 84 118 147 178 209 240}\n", + ) + if __name__ == "__main__": unittest.main() - diff --git a/src/pytorch/.gitignore b/src/pytorch/.gitignore index d22daff42d..49df78733c 100644 --- a/src/pytorch/.gitignore +++ b/src/pytorch/.gitignore @@ -9,3 +9,4 @@ !/COPYRIGHT !/module.type !/postprocessing +!/requirements.txt diff --git a/src/pytorch/requirements.txt b/src/pytorch/requirements.txt new file mode 100644 index 0000000000..40ea3fac70 --- /dev/null +++ b/src/pytorch/requirements.txt @@ -0,0 +1,4 @@ +# torch 2.7 is the first one to use cxx11 ABI for the PyPI wheels +#--extra-index-url=https://download.pytorch.org/whl/cpu +# the extra url should be specified by the user, to select a a more specific cpu build or a gpu one +torch>=2.7