diff --git a/README.md b/README.md index 7a5d6abf..c8767987 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Includes functionality for: * Much more! ## Documentation -Complete documentation for polymerist can be found on the [`polymerist` ReadTheDocs page](https://polymerist.readthedocs.io/en/docs/) +Complete documentation for polymerist can be found on the [`polymerist` ReadTheDocs page](https://polymerist.readthedocs.io/en/latest/) ## Examples Examples of how to import and invoke the core features of `polymerist` can be found in the accompanying [polymerist_examples repository](https://github.com/timbernat/polymerist_examples). diff --git a/devtools/conda-envs/dev-build.yml b/devtools/conda-envs/dev-build.yml index 997db53e..a19a471e 100644 --- a/devtools/conda-envs/dev-build.yml +++ b/devtools/conda-envs/dev-build.yml @@ -6,23 +6,24 @@ channels: dependencies: - _libgcc_mutex=0.1=conda_forge - _openmp_mutex=4.5=2_gnu - - absl-py=2.1.0=pyhd8ed1ab_1 + - _python_abi3_support=1.0=hd8ed1ab_2 + - absl-py=2.3.1=pyhd8ed1ab_0 - alabaster=1.0.0=pyhd8ed1ab_1 - - alsa-lib=1.2.13=hb9d3cd8_0 + - alsa-lib=1.2.14=hb9d3cd8_0 - ambertools=23.3=py311h9fea076_6 - annotated-types=0.7.0=pyhd8ed1ab_1 - - anyio=4.7.0=pyhd8ed1ab_0 - - anytree=2.12.1=pyhd8ed1ab_0 - - argon2-cffi=23.1.0=pyhd8ed1ab_1 - - argon2-cffi-bindings=21.2.0=py311h9ecbd09_5 + - anyio=4.10.0=pyhe01879c_0 + - anytree=2.13.0=pyhd8ed1ab_0 + - argon2-cffi=25.1.0=pyhd8ed1ab_0 + - argon2-cffi-bindings=25.1.0=py311h49ec1c0_0 - arpack=3.8.0=nompi_h0baa96a_101 - arrow=1.3.0=pyhd8ed1ab_1 - - ase=3.23.0=pyhd8ed1ab_0 + - ase=3.26.0=pyhd8ed1ab_0 + - astroid=3.3.11=py311h38be061_0 - asttokens=3.0.0=pyhd8ed1ab_1 - - astunparse=1.6.3=pyhd8ed1ab_2 - - async-lru=2.0.4=pyhd8ed1ab_1 - - attr=2.5.1=h166bdaf_1 - - attrs=24.3.0=pyh71513ae_0 + - astunparse=1.6.3=pyhd8ed1ab_3 + - async-lru=2.0.5=pyh29332c3_0 + - attrs=25.3.0=pyh71513ae_0 - aws-c-auth=0.7.31=h57bd9a3_0 - aws-c-cal=0.7.4=hfd43aa1_1 - aws-c-common=0.9.28=hb9d3cd8_0 @@ -36,70 +37,75 @@ dependencies: - aws-checksums=0.1.20=h756ea98_0 - aws-crt-cpp=0.28.3=h3e6eb3e_6 - aws-sdk-cpp=1.11.379=h9f1560d_11 - - azure-core-cpp=1.13.0=h935415a_0 - - azure-identity-cpp=1.8.0=hd126650_2 - - azure-storage-blobs-cpp=12.12.0=hd2e3451_0 - - azure-storage-common-cpp=12.7.0=h10ac4d7_1 - - azure-storage-files-datalake-cpp=12.11.0=h325d260_1 - - babel=2.16.0=pyhd8ed1ab_1 + - babel=2.17.0=pyhd8ed1ab_0 - backports=1.0=pyhd8ed1ab_5 - backports.tarfile=1.2.0=pyhd8ed1ab_1 - - beautifulsoup4=4.12.3=pyha770c72_1 - - bleach=6.2.0=pyhd8ed1ab_1 + - beautifulsoup4=4.13.4=pyha770c72_0 + - binutils_impl_linux-64=2.44=h4bf12b8_1 + - binutils_linux-64=2.44=h4852527_1 + - bleach=6.2.0=pyh29332c3_4 + - bleach-with-css=6.2.0=h82add2a_4 - blinker=1.9.0=pyhff2d567_0 - - blosc=1.21.6=hef167b5_0 - - boltons=24.0.0=pyhd8ed1ab_1 - - brotli=1.1.0=hb9d3cd8_2 - - brotli-bin=1.1.0=hb9d3cd8_2 - - brotli-python=1.1.0=py311hfdbb021_2 - - bson=0.5.9=pyhd8ed1ab_1 + - blosc=1.21.6=he440d0b_1 + - boltons=25.0.0=pyhd8ed1ab_0 + - brotli=1.1.0=hb9d3cd8_3 + - brotli-bin=1.1.0=hb9d3cd8_3 + - brotli-python=1.1.0=py311hfdbb021_3 + - bson=0.5.10=pyhd8ed1ab_0 - build=0.10.0=py311h06a4308_0 - bzip2=1.0.8=h4bc722e_7 - - c-ares=1.34.4=hb9d3cd8_0 - - c-blosc2=2.15.2=h68e2383_0 - - ca-certificates=2024.12.14=hbcca054_0 + - c-ares=1.34.5=hb9d3cd8_0 + - c-blosc2=2.17.1=h3122c55_0 + - ca-certificates=2025.10.5=hbd8a1cb_0 - cached-property=1.5.2=hd8ed1ab_1 - cached_property=1.5.2=pyha770c72_1 - - cachetools=5.5.0=pyhd8ed1ab_1 + - cachetools=6.1.0=pyhd8ed1ab_0 - cairo=1.18.0=hbb29018_2 - - certifi=2024.12.14=pyhd8ed1ab_0 + - certifi=2025.10.5=pyhd8ed1ab_0 - cffi=1.17.1=py311hf29c0ef_0 - - chardet=5.2.0=py311h38be061_2 - - charset-normalizer=3.4.0=pyhd8ed1ab_1 + - chardet=5.2.0=pyhd8ed1ab_3 + - charset-normalizer=3.4.3=pyhd8ed1ab_0 - cirpy=1.0.2=py_0 - - click=8.1.7=unix_pyh707e725_1 + - click=8.2.1=pyh707e725_0 - click-option-group=0.5.6=pyhd8ed1ab_0 - - cloudpickle=3.1.0=pyhd8ed1ab_1 + - cmake=4.1.0=hc85cc9f_0 - cmarkgfm=2024.11.20=py311h9ecbd09_0 - - codecov=2.1.13=pyhd8ed1ab_0 + - codecov=2.1.13=pyhd8ed1ab_1 - colorama=0.4.6=pyhd8ed1ab_1 - - comm=0.2.2=pyhd8ed1ab_1 - - contourpy=1.3.1=py311hd18a35c_0 - - coverage=7.6.9=py311h2dc5d0c_0 - - cryptography=44.0.0=py311hafd3f86_0 - - cudatoolkit=11.8.0=h4ba93d1_13 + - comm=0.2.3=pyhe01879c_0 + - contourpy=1.3.3=py311hdf67eae_1 + - coverage=7.10.4=py311h3778330_0 + - cpython=3.11.13=py311hd8ed1ab_0 + - cryptography=45.0.6=py311h8488d03_0 + - cuda-crt-tools=12.9.86=ha770c72_2 + - cuda-cudart=12.9.79=h5888daf_0 + - cuda-cudart_linux-64=12.9.79=h3f2d84a_0 + - cuda-nvcc-tools=12.9.86=he02047a_2 + - cuda-nvrtc=12.9.86=h5888daf_0 + - cuda-nvtx=12.9.79=h5888daf_0 + - cuda-nvvm-tools=12.9.86=h4bc722e_2 + - cuda-version=12.9=h4f385c5_3 + - cudnn=8.9.7.29=h092f7fd_3 - cycler=0.12.1=pyhd8ed1ab_1 - - cytoolz=1.0.1=py311h9ecbd09_0 - - dask-core=2024.12.1=pyhd8ed1ab_0 - - dask-jobqueue=0.9.0=pyhd8ed1ab_0 - dbus=1.13.6=h5008d03_3 - - debugpy=1.8.11=py311hfdbb021_0 - - decorator=5.1.1=pyhd8ed1ab_1 + - debugpy=1.8.16=py311hc665b79_0 + - decorator=5.2.1=pyhd8ed1ab_0 - defusedxml=0.7.1=pyhd8ed1ab_0 - - dgl=2.1.0=py311h1772aec_2 - - distributed=2024.12.1=pyhd8ed1ab_0 + - dgl=2.3.0=py311h1772aec_0 - docutils=0.21.2=pyhd8ed1ab_1 - - double-conversion=3.3.0=h59595ed_0 + - double-conversion=3.3.1=h5888daf_0 - ele=0.2.0=pyhd8ed1ab_0 - - entrypoints=0.4=pyhd8ed1ab_1 - - exceptiongroup=1.2.2=pyhd8ed1ab_1 - - executing=2.1.0=pyhd8ed1ab_1 - - expat=2.6.4=h5888daf_0 - - f90wrap=0.2.16=py311hcc61034_1 - - fftw=3.3.10=mpi_openmpi_h99e62ba_10 - - filelock=3.16.1=pyhd8ed1ab_1 - - flask=3.1.0=pyhff2d567_0 + - espaloma_charge=0.0.8=pyhd8ed1ab_3 + - exceptiongroup=1.3.0=pyhd8ed1ab_0 + - executing=2.2.0=pyhd8ed1ab_0 + - expat=2.7.1=hecca717_0 + - f90wrap=0.2.16=py311h262f814_2 + - fftw=3.3.10=nompi_hf1063bd_110 + - filelock=3.19.1=pyhd8ed1ab_0 + - flask=3.1.1=pyhd8ed1ab_0 - flatbuffers=24.3.25=h59595ed_0 + - flexcache=0.3=pyhd8ed1ab_1 + - flexparser=0.4=pyhd8ed1ab_1 - font-ttf-dejavu-sans-mono=2.37=hab24e00_0 - font-ttf-inconsolata=3.000=h77eed37_0 - font-ttf-source-code-pro=2.038=h77eed37_0 @@ -107,171 +113,163 @@ dependencies: - fontconfig=2.15.0=h7e30c49_1 - fonts-conda-ecosystem=1=0 - fonts-conda-forge=1=0 - - fonttools=4.55.3=py311h2dc5d0c_0 - - forcefield-utilities=0.3.0=pyhd8ed1ab_0 + - fonttools=4.59.1=py311h3778330_0 + - forcefield-utilities=0.4.0=pyhd8ed1ab_2 - foyer=1.0.0=pyhd8ed1ab_0 - fqdn=1.5.1=pyhd8ed1ab_1 - - freetype=2.12.1=h267a509_2 + - freetype=2.13.3=ha770c72_1 - freetype-py=2.3.0=pyhd8ed1ab_0 - - fsspec=2024.10.0=pyhd8ed1ab_1 + - fsspec=2025.7.0=pyhd8ed1ab_0 - gast=0.6.0=pyhd8ed1ab_0 - gawk=5.3.1=hcd3d067_0 - - gettext=0.22.5=he02047a_3 - - gettext-tools=0.22.5=he02047a_3 - - gflags=2.2.2=h5888daf_1005 + - gcc_impl_linux-64=13.4.0=h69c5793_4 + - gcc_linux-64=13.4.0=h621f4e2_11 + - gfortran_impl_linux-64=13.4.0=h847f9e2_4 + - gfortran_linux-64=13.4.0=h3b61c9e_11 - giflib=5.2.2=hd590300_0 - - glog=0.7.1=hbabe93e_0 - gmp=6.3.0=hac33072_2 - - gmpy2=2.1.5=py311h0f6cedb_3 - - gmso=0.12.4=pyhd8ed1ab_1 + - gmpy2=2.2.1=py311h0f6cedb_0 + - gmso=0.13.0=pyhd8ed1ab_0 - google-pasta=0.2.0=pyhd8ed1ab_2 - - graphite2=1.3.13=h59595ed_1003 - - greenlet=3.1.1=py311hfdbb021_1 + - graphite2=1.3.14=hecca717_2 + - greenlet=3.2.4=py311h1ddb823_0 - grpcio=1.62.2=py311ha6695c7_0 - gsl=2.7=he838d99_0 - - h11=0.14.0=pyhd8ed1ab_1 - - h2=4.1.0=pyhd8ed1ab_1 - - h5py=3.12.1=nompi_py311hb639ac4_103 + - gxx_impl_linux-64=13.4.0=haf17267_4 + - gxx_linux-64=13.4.0=he431e45_11 + - h11=0.16.0=pyhd8ed1ab_0 + - h2=4.2.0=pyhd8ed1ab_0 + - h5py=3.13.0=nompi_py311hb639ac4_100 - harfbuzz=9.0.0=hfac3d4d_0 - hdf4=4.2.15=h2a13503_7 - - hdf5=1.14.3=mpi_openmpi_h39ae36c_8 - - hpack=4.0.0=pyhd8ed1ab_1 - - httpcore=1.0.7=pyh29332c3_1 + - hdf5=1.14.3=nompi_h2d575fe_109 + - hpack=4.1.0=pyhd8ed1ab_0 + - httpcore=1.0.9=pyh29332c3_0 - httpx=0.28.1=pyhd8ed1ab_0 - - hyperframe=6.0.1=pyhd8ed1ab_1 + - hyperframe=6.1.0=pyhd8ed1ab_0 - icu=73.2=h59595ed_0 + - id=1.5.0=pyh29332c3_0 - idna=3.10=pyhd8ed1ab_1 - imagesize=1.4.1=pyhd8ed1ab_0 - - importlib-metadata=8.5.0=pyha770c72_1 - - importlib_resources=6.4.5=pyhd8ed1ab_1 + - importlib-metadata=8.7.0=pyhe01879c_1 + - importlib_resources=6.5.2=pyhd8ed1ab_0 - iniconfig=2.0.0=pyhd8ed1ab_1 - - ipykernel=6.29.5=pyh3099207_0 - - ipython=8.30.0=pyh707e725_0 + - ipykernel=6.30.1=pyh82676e8_0 + - ipython=9.4.0=pyhfa0c392_0 + - ipython_pygments_lexers=1.1.1=pyhd8ed1ab_0 - ipywidgets=8.0.4=pyhd8ed1ab_0 - isoduration=20.11.0=pyhd8ed1ab_1 - itsdangerous=2.2.0=pyhd8ed1ab_1 - jaraco.classes=3.4.0=pyhd8ed1ab_2 - jaraco.context=6.0.1=pyhd8ed1ab_0 - - jaraco.functools=4.1.0=pyhd8ed1ab_0 + - jaraco.functools=4.3.0=pyhd8ed1ab_0 - jedi=0.19.2=pyhd8ed1ab_1 - - jeepney=0.8.0=pyhd8ed1ab_0 - - jinja2=3.1.4=pyhd8ed1ab_1 - - joblib=1.4.2=pyhd8ed1ab_1 - - json5=0.10.0=pyhd8ed1ab_1 + - jeepney=0.9.0=pyhd8ed1ab_0 + - jinja2=3.1.6=pyhd8ed1ab_0 + - joblib=1.5.1=pyhd8ed1ab_0 + - json5=0.12.1=pyhd8ed1ab_0 - jsonpointer=3.0.0=py311h38be061_1 - - jsonschema=4.23.0=pyhd8ed1ab_1 - - jsonschema-specifications=2024.10.1=pyhd8ed1ab_1 - - jsonschema-with-format-nongpl=4.23.0=hd8ed1ab_1 - - jupyter-lsp=2.2.5=pyhd8ed1ab_1 + - jsonschema=4.25.0=pyhe01879c_0 + - jsonschema-specifications=2025.4.1=pyh29332c3_0 + - jsonschema-with-format-nongpl=4.25.0=he01879c_0 + - jupyter-lsp=2.2.6=pyhe01879c_0 - jupyter_client=8.6.3=pyhd8ed1ab_1 - - jupyter_core=5.7.2=pyh31011fe_1 - - jupyter_events=0.11.0=pyhd8ed1ab_0 - - jupyter_server=2.14.2=pyhd8ed1ab_1 + - jupyter_core=5.8.1=pyh31011fe_0 + - jupyter_events=0.12.0=pyh29332c3_0 + - jupyter_server=2.16.0=pyhe01879c_0 - jupyter_server_terminals=0.5.3=pyhd8ed1ab_1 - - jupyterlab=4.3.4=pyhd8ed1ab_0 + - jupyterlab=4.4.6=pyhd8ed1ab_0 - jupyterlab_pygments=0.3.0=pyhd8ed1ab_2 - jupyterlab_server=2.27.3=pyhd8ed1ab_1 - - jupyterlab_widgets=3.0.13=pyhd8ed1ab_1 - - keras=3.7.0=pyh753f3f9_1 - - kernel-headers_linux-64=3.10.0=he073ed8_18 - - keyring=25.5.0=pyha804496_1 - - keyutils=1.6.1=h166bdaf_0 - - kim-api=2.2.1=h5c8ed42_0 - - kiwisolver=1.4.7=py311hd18a35c_0 + - jupyterlab_widgets=3.0.15=pyhd8ed1ab_0 + - keras=3.11.2=pyh753f3f9_0 + - kernel-headers_linux-64=4.18.0=he073ed8_8 + - keyring=25.6.0=pyha804496_0 + - keyutils=1.6.3=hb9d3cd8_0 + - kim-api=2.4.1=hb61eb52_0 + - kiwisolver=1.4.9=py311h724c32c_0 - krb5=1.21.3=h659f571_0 - - lammps=2024.08.29=cpu_py311_h81f0c5c_mpi_openmpi_0 - - lark=1.2.2=pyhd8ed1ab_0 - - lcms2=2.16=hb7c19ff_0 - - ld_impl_linux-64=2.43=h712a8e2_2 - - lerc=4.0.0=h27087fc_0 + - lammps=2024.08.29=cpu_py311_hc2ea7de_nompi_1 + - lark=1.2.2=pyhd8ed1ab_1 + - lcms2=2.17=h717163a_0 + - ld_impl_linux-64=2.44=h1423503_1 + - lerc=4.0.0=h0aef613_1 - libabseil=20240116.2=cxx17_he02047a_1 - - libaec=1.1.3=h59595ed_0 - - libarrow=17.0.0=h200f1f0_15_cpu - - libarrow-acero=17.0.0=h5888daf_15_cpu - - libarrow-dataset=17.0.0=h5888daf_15_cpu - - libarrow-substrait=17.0.0=hf54134d_15_cpu - - libasprintf=0.22.5=he8f35ee_3 - - libasprintf-devel=0.22.5=he8f35ee_3 - - libblas=3.9.0=26_linux64_openblas + - libaec=1.1.4=h3f801dc_0 + - libasprintf=0.25.1=h3f43e3d_1 + - libblas=3.9.0=34_h59b9bed_openblas - libboost=1.82.0=h6fcfa73_6 - libboost-python=1.82.0=py311h92ebd52_6 - - libbrotlicommon=1.1.0=hb9d3cd8_2 - - libbrotlidec=1.1.0=hb9d3cd8_2 - - libbrotlienc=1.1.0=hb9d3cd8_2 - - libcap=2.71=h39aace5_0 - - libcblas=3.9.0=26_linux64_openblas - - libclang-cpp18.1=18.1.8=default_hf981a13_5 + - libbrotlicommon=1.1.0=hb9d3cd8_3 + - libbrotlidec=1.1.0=hb9d3cd8_3 + - libbrotlienc=1.1.0=hb9d3cd8_3 + - libcblas=3.9.0=34_he106b2a_openblas + - libclang-cpp18.1=18.1.8=default_hddf928d_13 - libclang13=19.1.2=default_h9c6a7e4_1 - - libcrc32c=1.1.2=h9c3ff4c_0 - - libcups=2.3.3=h4637d8d_4 - - libcurl=8.11.1=h332b0f4_0 - - libdeflate=1.23=h4ddbbb0_0 - - libdrm=2.4.124=hb9d3cd8_0 - - libedit=3.1.20191231=he28a2e2_2 + - libcublas=12.9.1.4=h9ab20c4_0 + - libcufft=11.4.1.4=h5888daf_0 + - libcups=2.3.3=hb8b1518_5 + - libcurand=10.3.10.19=h9ab20c4_0 + - libcurl=8.14.1=h332b0f4_0 + - libcusolver=11.7.5.82=h9ab20c4_1 + - libcusparse=12.5.10.65=h5888daf_1 + - libdeflate=1.24=h86f0d12_0 + - libdrm=2.4.125=hb9d3cd8_0 + - libedit=3.1.20250104=pl5321h7949ede_0 - libegl=1.7.0=ha4b6fd6_2 - libev=4.33=hd590300_2 - - libevent=2.1.12=hf998b51_1 - - libexpat=2.6.4=h5888daf_0 - - libfabric=1.22.0=ha770c72_3 - - libfabric1=1.22.0=ha594dbc_3 - - libffi=3.4.2=h7f98852_5 - - libflint=3.1.2=h6fb9888_101 - - libgcc=14.2.0=h77fa898_1 - - libgcc-ng=14.2.0=h69a702a_1 - - libgcrypt-lib=1.11.0=hb9d3cd8_2 - - libgettextpo=0.22.5=he02047a_3 - - libgettextpo-devel=0.22.5=he02047a_3 - - libgfortran=14.2.0=h69a702a_1 - - libgfortran-ng=14.2.0=h69a702a_1 - - libgfortran5=14.2.0=hd5240d6_1 + - libexpat=2.7.1=hecca717_0 + - libffi=3.4.6=h2dba641_1 + - libflint=3.2.2=h754cb6e_0 + - libfreetype=2.13.3=ha770c72_1 + - libfreetype6=2.13.3=h48d6fc4_1 + - libgcc=15.1.0=h767d61c_4 + - libgcc-devel_linux-64=13.4.0=hba01cd7_104 + - libgcc-ng=15.1.0=h69a702a_4 + - libgettextpo=0.25.1=h3f43e3d_1 + - libgfortran=15.1.0=h69a702a_4 + - libgfortran-ng=15.1.0=h69a702a_4 + - libgfortran5=15.1.0=hcea5267_4 - libgl=1.7.0=ha4b6fd6_2 - libglib=2.80.2=hf974151_0 - libglvnd=1.7.0=ha4b6fd6_2 - libglx=1.7.0=ha4b6fd6_2 - - libgomp=14.2.0=h77fa898_1 - - libgoogle-cloud=2.29.0=h435de7b_0 - - libgoogle-cloud-storage=2.29.0=h0121fbd_0 - - libgpg-error=1.51=hbd13f7d_1 + - libgomp=15.1.0=h767d61c_4 - libgrpc=1.62.2=h15f2491_0 - - libhwloc=2.11.2=default_he43201b_1000 - - libiconv=1.17=hd590300_2 - - libidn2=2.3.7=hd590300_0 - - libjpeg-turbo=3.0.0=hd590300_1 - - liblapack=3.9.0=26_linux64_openblas - - liblapacke=3.9.0=26_linux64_openblas + - libiconv=1.18=h3b78370_2 + - libidn2=2.3.8=ha4ef2c3_0 + - libjpeg-turbo=3.1.0=hb9d3cd8_0 + - liblapack=3.9.0=34_h7ac8fdf_openblas + - liblapacke=3.9.0=34_he2f377e_openblas - libllvm18=18.1.8=h8b73ec9_2 - libllvm19=19.1.2=ha7bfdaf_0 - - liblzma=5.6.3=hb9d3cd8_1 - - liblzma-devel=5.6.3=hb9d3cd8_1 - - libnetcdf=4.9.2=mpi_openmpi_ha1e512f_14 + - liblzma=5.8.1=hb9d3cd8_2 + - liblzma-devel=5.8.1=hb9d3cd8_2 + - libnetcdf=4.9.2=nompi_h135f659_114 - libnghttp2=1.64.0=h161d5f1_0 - - libnl=3.11.0=hb9d3cd8_0 - - libnsl=2.0.1=hd590300_0 - - libopenblas=0.3.28=pthreads_h94d23a6_1 + - libnsl=2.0.1=hb9d3cd8_1 + - libnvjitlink=12.9.86=h5888daf_1 + - libopenblas=0.3.30=pthreads_h94d23a6_2 - libopengl=1.7.0=ha4b6fd6_2 - - libparquet=17.0.0=h39682fd_15_cpu - - libpciaccess=0.18=hd590300_0 - - libpnetcdf=1.13.0=mpi_openmpi_h521bef2_1 - - libpng=1.6.44=hadc24fc_0 - - libpq=16.6=h035377e_1 + - libpciaccess=0.18=hb9d3cd8_0 + - libpng=1.6.50=h421ea60_1 + - libpq=16.10=h1cdf469_0 - libprotobuf=4.25.3=hd5b35b9_1 - libre2-11=2023.09.01=h5a48ba9_2 + - libsanitizer=13.4.0=h14bf0c3_4 - libsodium=1.0.20=h4ab18f5_0 - - libsqlite=3.47.2=hee588c1_0 - - libssh2=1.11.1=hf672d98_0 - - libstdcxx=14.2.0=hc0a3c3a_1 - - libstdcxx-ng=14.2.0=h4852527_1 - - libsystemd0=256.9=h2774228_0 - - libthrift=0.20.0=h0e7cc3e_1 - - libtiff=4.7.0=hd9ff511_3 + - libsqlite=3.50.4=h0c1763c_0 + - libssh2=1.11.1=hcf80075_0 + - libstdcxx=15.1.0=h8f9b012_4 + - libstdcxx-devel_linux-64=13.4.0=hba01cd7_104 + - libstdcxx-ng=15.1.0=h4852527_4 + - libtiff=4.7.0=h8261f1e_6 - libtorch=2.3.1=cpu_generic_h970db74_0 - - libudev1=256.9=h9a4d06a_2 - libunistring=0.9.10=h7f98852_0 - - liburing=2.6=h297d8ca_0 - - libutf8proc=2.8.0=hf23e847_1 + - liburing=2.7=h434a139_0 - libuuid=2.38.1=h0b41bf4_0 - - libuv=1.49.2=hb9d3cd8_0 - - libwebp-base=1.4.0=hd590300_0 + - libuv=1.51.0=hb03c661_1 + - libwebp-base=1.6.0=hd42ef1d_0 - libxcb=1.17.0=h8a09558_0 - libxcrypt=4.4.36=hd590300_1 - libxkbcommon=1.7.0=h2c5496b_1 @@ -279,168 +277,161 @@ dependencies: - libxslt=1.1.39=h76b75d6_0 - libzip=1.11.2=h6991a6a_0 - libzlib=1.3.1=hb9d3cd8_2 - - lightning-utilities=0.11.9=pyhd8ed1ab_1 - - locket=1.0.0=pyhd8ed1ab_0 + - lightning-utilities=0.15.2=pyhd8ed1ab_0 - lxml=5.3.0=py311hcfaa980_2 - - lz4-c=1.9.4=hcb278e6_0 - - markdown=3.6=pyhd8ed1ab_0 - - markdown-it-py=3.0.0=pyhd8ed1ab_1 + - lz4-c=1.10.0=h5888daf_1 + - make=4.4.1=hb9d3cd8_2 + - markdown=3.8.2=pyhd8ed1ab_0 + - markdown-it-py=4.0.0=pyhd8ed1ab_0 - markupsafe=3.0.2=py311h2dc5d0c_1 - - matplotlib=3.10.0=py311h38be061_0 - - matplotlib-base=3.10.0=py311h2b939e6_0 + - matplotlib=3.10.5=py311h38be061_0 + - matplotlib-base=3.10.5=py311h0f3be63_0 - matplotlib-inline=0.1.7=pyhd8ed1ab_1 - - mbuild=1.1.0=pyhd8ed1ab_0 + - mbuild=1.2.0=pyhd8ed1ab_0 - mda-xdrlib=0.2.0=pyhd8ed1ab_1 - - mdtraj=1.10.2=py311h2ed89a0_0 + - mdtraj=1.11.0=np2py311hb255e1c_2 - mdurl=0.1.2=pyhd8ed1ab_1 - metis=5.1.1=h59595ed_2 - - mistune=3.0.2=pyhd8ed1ab_1 + - mistune=3.1.3=pyh29332c3_0 - ml_dtypes=0.3.2=py311h320fe9a_0 - - mlip=3.0=mpi_openmpi_h312e9eb_2 - - more-itertools=10.5.0=pyhd8ed1ab_1 + - mlip=3.0=nompi_h841f795_2 + - more-itertools=10.7.0=pyhd8ed1ab_0 - mpc=1.3.1=h24ddda3_1 - mpfr=4.2.1=h90cbb55_3 - - mpi=1.0=openmpi - - mpi4py=4.0.1=py311ha982e2a_0 - mpmath=1.3.0=pyhd8ed1ab_1 - - msgpack-python=1.1.0=py311hd18a35c_0 - - munkres=1.1.4=pyh9f0ad1d_0 + - munkres=1.1.4=pyhd8ed1ab_1 - mysql-common=8.3.0=h70512c7_5 - mysql-libs=8.3.0=ha479ceb_5 - - n2p2=2.2.0=mpi_openmpi_py311_h819e8d0_106 - - namex=0.0.8=pyhd8ed1ab_1 - - nbclient=0.10.1=pyhd8ed1ab_0 - - nbconvert-core=7.16.4=pyhff2d567_2 + - n2p2=2.2.0=nompi_py311_h29ebb03_7 + - namex=0.1.0=pyhd8ed1ab_0 + - nbclient=0.10.2=pyhd8ed1ab_0 + - nbconvert-core=7.16.6=pyh29332c3_0 - nbformat=5.10.4=pyhd8ed1ab_1 - - ncurses=6.5=he02047a_1 + - nbval=0.11.0=pyhd8ed1ab_1 + - nccl=2.27.7.1=h49b9d9a_0 + - ncurses=6.5=h2d0b736_3 - nest-asyncio=1.6.0=pyhd8ed1ab_1 - - netcdf-fortran=4.6.1=mpi_openmpi_h2e02aee_8 - - networkx=3.4.2=pyh267e887_2 + - netcdf-fortran=4.6.1=nompi_h22f9119_108 + - networkx=3.5=pyhe01879c_0 - nglview=3.0.6=pyhba93850_0 - - nh3=0.2.20=py311h9e33e62_0 + - nh3=0.3.0=py39hd511f7d_0 - nomkl=1.0=h5ca1d4c_0 - - notebook=7.3.1=pyhd8ed1ab_0 + - notebook=7.4.5=pyhd8ed1ab_0 - notebook-shim=0.2.4=pyhd8ed1ab_1 - numexpr=2.10.2=py311h38b10cd_100 - numpy=1.26.4=py311h64a7726_0 - - ocl-icd=2.3.2=hb9d3cd8_2 + - ocl-icd=2.3.3=hb9d3cd8_0 - ocl-icd-system=1.0.0=1 - openbabel=3.1.1=py311h8b422cb_9 - - opencl-headers=2024.10.24=h5888daf_0 - - openeye-toolkits=2024.2.0=py311_0 - - openff-amber-ff-ports=0.0.4=pyhca7485f_0 - - openff-forcefields=2024.09.0=pyhff2d567_0 - - openff-interchange=0.4.0=pyhd8ed1ab_0 - - openff-interchange-base=0.4.0=pyhd8ed1ab_0 - - openff-nagl=0.5.0=pyhd8ed1ab_0 - - openff-nagl-base=0.5.0=pyhd8ed1ab_0 - - openff-nagl-models=0.3.0=pyhd8ed1ab_1 - - openff-recharge=0.5.2=pyhd8ed1ab_0 - - openff-toolkit=0.16.7=pyhd8ed1ab_0 - - openff-toolkit-base=0.16.7=pyhd8ed1ab_0 - - openff-units=0.2.2=pyhd8ed1ab_1 - - openff-utilities=0.1.13=pyhd8ed1ab_0 - - openjpeg=2.5.3=h5fbd93e_0 - - openmm=8.2.0=py311he040c58_0 - - openmpi=5.0.6=hd45feaf_100 - - openssl=3.4.0=hb9d3cd8_0 + - opencl-headers=2025.06.13=h5888daf_0 + - openeye-toolkits=2025.1.1=py311_0 + - openff-amber-ff-ports=0.0.4=pyhd8ed1ab_1 + - openff-forcefields=2024.09.0=pyhd8ed1ab_1 + - openff-interchange=0.4.4=pyhd8ed1ab_1 + - openff-interchange-base=0.4.4=pyhd8ed1ab_1 + - openff-nagl=0.5.2=pyhd8ed1ab_2 + - openff-nagl-base=0.5.2=pyhd8ed1ab_2 + - openff-nagl-models=0.4.0=pyhd8ed1ab_0 + - openff-toolkit=0.16.10=pyhd8ed1ab_0 + - openff-toolkit-base=0.16.10=pyhd8ed1ab_0 + - openff-units=0.3.1=pyhd8ed1ab_2 + - openff-utilities=0.1.15=pyhd8ed1ab_1 + - openjpeg=2.5.3=h55fea9a_1 + - openmm=8.3.1=py311h1c77f12_0 + - openssl=3.6.0=h26f9b46_0 - opt_einsum=3.4.0=pyhd8ed1ab_1 - - optree=0.13.1=py311hd18a35c_1 - - orc=2.0.2=h669347b_0 - - overrides=7.7.0=pyhd8ed1ab_0 - - packaging=24.2=pyhd8ed1ab_2 + - optree=0.17.0=py311hdf67eae_0 + - overrides=7.7.0=pyhd8ed1ab_1 + - packaging=25.0=pyh29332c3_1 - packmol=20.15.1=hc8b2c43_1 - - pandas=2.2.3=py311h7db5c69_1 + - pandas=2.3.1=py311hed34c8f_0 - pandocfilters=1.5.0=pyhd8ed1ab_0 - - panedr=0.8.0=pyhd8ed1ab_1 - - parmed=4.3.0=py311h8cc7b42_0 + - parmed=4.3.0=py311h8cc7b42_2 - parso=0.8.4=pyhd8ed1ab_1 - - partd=1.4.2=pyhd8ed1ab_0 - pcre2=10.43=hcad00b1_0 - perl=5.32.1=7_hd590300_perl5 - pexpect=4.9.0=pyhd8ed1ab_1 - pickleshare=0.7.5=pyhd8ed1ab_1004 - - pillow=11.0.0=py311h49e9ac3_0 - - pint=0.23=pyhd8ed1ab_1 - - pip=24.3.1=pyh8b19718_2 - - pixman=0.44.2=h29eaf8c_0 + - pillow=11.3.0=py311h1322bbf_0 + - pint=0.24.4=pyhe01879c_2 + - pip=25.2=pyh8b19718_0 + - pixman=0.46.4=h54a6638_1 - pkg-config=0.29.2=h4bc722e_1009 - - pkginfo=1.12.0=pyhd8ed1ab_1 - - pkgutil-resolve-name=1.3.10=pyhd8ed1ab_2 - - platformdirs=4.3.6=pyhd8ed1ab_1 - - pluggy=1.5.0=pyhd8ed1ab_1 - - plumed=2.9.2=mpi_openmpi_h02da92d_0 - - prometheus_client=0.21.1=pyhd8ed1ab_0 - - prompt-toolkit=3.0.48=pyha770c72_1 + - platformdirs=4.3.8=pyhe01879c_0 + - pluggy=1.6.0=pyhd8ed1ab_0 + - plumed=2.9.2=nompi_hda5c677_100 + - prometheus_client=0.22.1=pyhd8ed1ab_0 + - prompt-toolkit=3.0.51=pyha770c72_0 - protobuf=4.25.3=py311hbffca5d_1 - - psutil=6.1.0=py311h9ecbd09_0 + - psutil=7.0.0=py311h9ecbd09_0 - pthread-stubs=0.4=hb9d3cd8_1002 - ptyprocess=0.7.0=pyhd8ed1ab_1 - - pubchempy=1.0.4=py_0 + - pubchempy=1.0.4=pyhd8ed1ab_1 - pure_eval=0.2.3=pyhd8ed1ab_1 - py-cpuinfo=9.0.0=pyhd8ed1ab_1 - - py3dmol=2.4.2=pyhd8ed1ab_0 - - pyarrow=17.0.0=py311hbd00459_2 - - pyarrow-core=17.0.0=py311h4854187_2_cpu + - py3dmol=2.5.2=pyhd8ed1ab_0 - pycairo=1.27.0=py311h124c5f0_0 - pycparser=2.22=pyh29332c3_1 - - pydantic=2.10.3=pyh3cfb1c2_0 - - pydantic-core=2.27.1=py311h9e33e62_0 + - pydantic=2.11.7=pyh3cfb1c2_0 + - pydantic-core=2.33.2=py311hdae7d1d_0 - pyedr=0.8.0=pyhd8ed1ab_1 - - pygments=2.18.0=pyhd8ed1ab_1 - - pyparsing=3.2.0=pyhd8ed1ab_2 + - pygments=2.19.2=pyhd8ed1ab_0 + - pyparsing=3.2.3=pyhe01879c_2 - pyproject_hooks=1.2.0=pyhd8ed1ab_1 - pyside6=6.7.2=py311h9053184_4 - pysocks=1.7.1=pyha55dd90_7 - - pytables=3.10.1=py311h6d53d72_4 - - pytest=8.3.4=pyhd8ed1ab_1 - - pytest-cov=6.0.0=pyhd8ed1ab_1 + - pytables=3.10.2=py311h6d53d72_2 + - pytest=8.4.1=pyhd8ed1ab_0 + - pytest-cov=6.2.1=pyhd8ed1ab_0 - python=3.11.0=he550d4f_1_cpython - python-constraint=1.4.0=pyhff2d567_1 - - python-dateutil=2.9.0.post0=pyhff2d567_1 - - python-fastjsonschema=2.21.1=pyhd8ed1ab_0 - - python-flatbuffers=24.3.25=pyhe33e51e_1 + - python-dateutil=2.9.0.post0=pyhe01879c_2 + - python-fastjsonschema=2.21.2=pyhe01879c_0 + - python-flatbuffers=25.2.10=pyhbc23db3_0 + - python-gil=3.11.13=hd8ed1ab_0 - python-json-logger=2.0.7=pyhd8ed1ab_0 - - python-symengine=0.13.0=py311hb815d42_2 - - python-tzdata=2024.2=pyhd8ed1ab_1 - - python_abi=3.11=5_cp311 + - python-symengine=0.14.0=py311h7b351a7_1 + - python-tzdata=2025.2=pyhd8ed1ab_0 + - python_abi=3.11=8_cp311 - pytorch=2.3.1=cpu_generic_py311h8ca351a_0 - - pytorch-lightning=2.4.0=pyh101cb37_1 - - pytz=2024.1=pyhd8ed1ab_0 - - pyyaml=6.0.2=py311h9ecbd09_1 - - pyzmq=26.2.0=py311h7deb3e3_3 + - pytorch-lightning=2.5.3=pyh2a12c56_0 + - pytz=2025.2=pyhd8ed1ab_0 + - pyyaml=6.0.2=py311h2dc5d0c_2 + - pyzmq=27.0.1=py311hc251a9f_0 - qhull=2020.2=h434a139_5 - qt6-main=6.7.2=h7d13b96_3 - quippy=0.9.14=py311h4189ce2_0 - rdkit=2023.09.6=py311h4c2f14b_0 - - rdma-core=55.0=h5888daf_0 - re2=2023.09.01=h7f4b329_2 - - readline=8.2=h8228510_1 + - readline=8.2=h8c095d6_2 - readme_renderer=44.0=pyhd8ed1ab_1 - - referencing=0.35.1=pyhd8ed1ab_1 - - reportlab=4.2.5=py311h9ecbd09_0 - - requests=2.32.3=pyhd8ed1ab_1 + - referencing=0.36.2=pyh29332c3_0 + - reportlab=4.4.1=py311h9ecbd09_0 + - requests=2.32.4=pyhd8ed1ab_0 - requests-toolbelt=1.0.0=pyhd8ed1ab_1 - rfc3339-validator=0.1.4=pyhd8ed1ab_1 - rfc3986=2.0.0=pyhd8ed1ab_1 - rfc3986-validator=0.1.1=pyh9f0ad1d_0 - - rich=13.9.4=pyhd8ed1ab_1 + - rfc3987-syntax=1.1.0=pyhe01879c_1 + - rhash=1.4.6=hb9d3cd8_1 + - rich=14.1.0=pyhe01879c_0 - rlpycairo=0.2.0=pyhd8ed1ab_0 - - rpds-py=0.22.3=py311h9e33e62_0 + - roman-numerals-py=3.1.0=pyhd8ed1ab_0 + - rpds-py=0.27.0=py311h902ca64_0 - s2n=1.5.5=h3931f03_0 - - scipy=1.13.1=py311h517d4fd_0 + - scipy=1.16.1=py311h33d6a90_0 - secretstorage=3.3.3=py311h38be061_3 - send2trash=1.8.3=pyh0d859eb_1 - - setuptools=75.6.0=pyhff2d567_1 - - six=1.17.0=pyhd8ed1ab_0 - - sleef=3.7=h1b44611_2 + - setuptools=80.9.0=pyhff2d567_0 + - six=1.17.0=pyhe01879c_1 + - sleef=3.8=h1b44611_0 - smirnoff99frosst=1.1.0=pyh44b312d_0 - - snappy=1.2.1=h8bd8927_1 + - snappy=1.2.2=h03e3b7b_0 - sniffio=1.3.1=pyhd8ed1ab_1 - - snowballstemmer=2.2.0=pyhd8ed1ab_0 - - sortedcontainers=2.4.0=pyhd8ed1ab_0 - - soupsieve=2.5=pyhd8ed1ab_1 - - sphinx=8.1.3=pyhd8ed1ab_1 + - snowballstemmer=3.0.1=pyhd8ed1ab_0 + - soupsieve=2.7=pyhd8ed1ab_0 + - sphinx=8.2.3=pyhd8ed1ab_0 + - sphinx-autoapi=3.6.0=pyhd8ed1ab_0 - sphinx_rtd_theme=3.0.1=pyha770c72_0 - sphinxcontrib-applehelp=2.0.0=pyhd8ed1ab_1 - sphinxcontrib-devhelp=2.0.0=pyhd8ed1ab_1 @@ -449,45 +440,42 @@ dependencies: - sphinxcontrib-jsmath=1.0.1=pyhd8ed1ab_1 - sphinxcontrib-qthelp=2.0.0=pyhd8ed1ab_1 - sphinxcontrib-serializinghtml=1.1.10=pyhd8ed1ab_1 - - sqlalchemy=2.0.36=py311h9ecbd09_0 - - sqlite=3.47.2=h9eae976_0 + - sqlalchemy=2.0.43=py311h49ec1c0_0 - stack_data=0.6.3=pyhd8ed1ab_1 - - symengine=0.13.0=h95c1f20_3 - - sympy=1.13.3=pypyh2585a3b_103 - - sysroot_linux-64=2.17=h4a8ded7_18 - - tblib=3.0.0=pyhd8ed1ab_1 + - stdlib-list=0.11.1=pyhd8ed1ab_0 + - symengine=0.14.0=h064106a_1 + - sympy=1.14.0=pyh2585a3b_105 + - sysroot_linux-64=2.28=h4ee821c_8 - tensorboard=2.16.2=pyhd8ed1ab_0 - tensorboard-data-server=0.7.0=py311hafd3f86_2 - - tensorflow=2.16.2=cpu_py311h6ac8430_0 - - tensorflow-base=2.16.2=cpu_py311h7888847_0 - - tensorflow-estimator=2.16.2=cpu_py311hbc9741f_0 - - termcolor=2.5.0=pyhd8ed1ab_1 + - tensorflow=2.16.2=cuda120py311h51447cc_0 + - tensorflow-base=2.16.2=cuda120py311h8a6ca57_0 + - tensorflow-estimator=2.16.2=cuda120py311h0c188d0_0 + - termcolor=3.1.0=pyhd8ed1ab_0 - terminado=0.18.1=pyh0d859eb_0 - tinycss2=1.4.0=pyhd8ed1ab_0 - - tk=8.6.13=noxft_h4845f30_101 + - tk=8.6.13=noxft_hd72426e_102 - toml=0.10.2=pyhd8ed1ab_1 - - tomli=2.2.1=pyhd8ed1ab_1 - - toolz=1.0.0=pyhd8ed1ab_1 + - tomli=2.2.1=pyhe01879c_2 - torchdata=0.7.1=py311h34085b1_7 - - torchmetrics=1.5.2=pyhd8ed1ab_1 - - tornado=6.4.2=py311h9ecbd09_0 - - tqdm=4.67.1=pyhd8ed1ab_0 + - torchmetrics=1.8.1=pyhd8ed1ab_0 + - tornado=6.5.2=py311h49ec1c0_0 + - tqdm=4.67.1=pyhd8ed1ab_1 - traitlets=5.14.3=pyhd8ed1ab_1 - - treelib=1.7.0=pyhd8ed1ab_0 - - twine=6.0.1=pyhd8ed1ab_1 - - types-python-dateutil=2.9.0.20241206=pyhd8ed1ab_0 - - typing-extensions=4.12.2=hd8ed1ab_1 - - typing_extensions=4.12.2=pyha770c72_1 + - treelib=1.7.1=pyhd8ed1ab_0 + - twine=6.1.0=pyh29332c3_0 + - types-python-dateutil=2.9.0.20250809=pyhd8ed1ab_0 + - typing-extensions=4.14.1=h4440ef1_0 + - typing-inspection=0.4.1=pyhd8ed1ab_0 + - typing_extensions=4.14.1=pyhe01879c_0 - typing_utils=0.1.0=pyhd8ed1ab_1 - - tzdata=2024b=hc8b5060_0 - - ucc=1.3.0=h0f835a6_3 - - ucx=1.17.0=h53fb5aa_4 - - unicodedata2=15.1.0=py311h9ecbd09_1 - - unyt=3.0.3=pyhd8ed1ab_2 + - tzdata=2025b=h78e105d_0 + - unicodedata2=16.0.0=py311h9ecbd09_0 + - unyt=3.0.4=pyhd8ed1ab_0 - uri-template=1.3.0=pyhd8ed1ab_1 - - urllib3=2.2.3=pyhd8ed1ab_1 + - urllib3=2.5.0=pyhd8ed1ab_0 - voro=0.4.6=h00ab1b0_0 - - wayland=1.23.1=h3e06ad9_0 + - wayland=1.24.0=h3e06ad9_0 - wcwidth=0.2.13=pyhd8ed1ab_1 - webcolors=24.11.1=pyhd8ed1ab_0 - webencodings=0.5.1=pyhd8ed1ab_3 @@ -495,42 +483,38 @@ dependencies: - werkzeug=3.1.3=pyhd8ed1ab_1 - wget=1.21.4=hda4d442_0 - wheel=0.45.1=pyhd8ed1ab_1 - - widgetsnbextension=4.0.13=pyhd8ed1ab_1 - - wrapt=1.17.0=py311h9ecbd09_0 - - xcb-util=0.4.1=hb711507_2 + - widgetsnbextension=4.0.14=pyhd8ed1ab_0 + - wrapt=1.17.3=py311h49ec1c0_0 + - xcb-util=0.4.1=h4f16b4b_2 - xcb-util-cursor=0.1.5=hb9d3cd8_0 - xcb-util-image=0.4.0=hb711507_2 - xcb-util-keysyms=0.4.1=hb711507_0 - xcb-util-renderutil=0.3.10=hb711507_0 - xcb-util-wm=0.4.2=hb711507_0 - - xkeyboard-config=2.43=hb9d3cd8_0 + - xkeyboard-config=2.45=hb9d3cd8_0 - xmltodict=0.14.2=pyhd8ed1ab_1 - xorg-libice=1.1.2=hb9d3cd8_0 - - xorg-libsm=1.2.5=he73a12e_0 - - xorg-libx11=1.8.10=h4f16b4b_1 + - xorg-libsm=1.2.6=he73a12e_0 + - xorg-libx11=1.8.12=h4f16b4b_0 - xorg-libxau=1.0.12=hb9d3cd8_0 - xorg-libxdmcp=1.1.5=hb9d3cd8_0 - xorg-libxext=1.3.6=hb9d3cd8_0 - xorg-libxrender=0.9.12=hb9d3cd8_0 - xorg-libxt=1.3.1=hb9d3cd8_0 - - xz=5.6.3=hbcc6ac9_1 - - xz-gpl-tools=5.6.3=hbcc6ac9_1 - - xz-tools=5.6.3=hb9d3cd8_1 - - yaml=0.2.5=h7f98852_2 + - xz=5.8.1=hbcc6ac9_2 + - xz-gpl-tools=5.8.1=hbcc6ac9_2 + - xz-tools=5.8.1=hb9d3cd8_2 + - yaml=0.2.5=h280c20c_3 - zeromq=4.3.5=h3b0a872_7 - - zict=3.0.0=pyhd8ed1ab_1 - - zipp=3.21.0=pyhd8ed1ab_1 + - zipp=3.23.0=pyhd8ed1ab_0 - zlib=1.3.1=hb9d3cd8_2 - - zlib-ng=2.2.2=h5888daf_0 - - zstandard=0.23.0=py311hbc35293_1 - - zstd=1.5.6=ha6fb4c9_0 + - zlib-ng=2.2.5=hde8ca8f_0 + - zstandard=0.23.0=py311h9ecbd09_2 + - zstd=1.5.7=hb8e6e7a_2 - pip: - amberutils==21.0 - - cftime==1.6.4.post1 - edgembar==0.2 - - espaloma-charge==0.0.8 - mmpbsa-py==16.0 - - netcdf4==1.7.2 - packmol-memgen==2023.2.24 - pdb4amber==22.0 - pymsmt==22.0 diff --git a/devtools/conda-envs/release-build.yml b/devtools/conda-envs/release-build.yml index 9c8fd82e..948cd5e2 100644 --- a/devtools/conda-envs/release-build.yml +++ b/devtools/conda-envs/release-build.yml @@ -6,6 +6,7 @@ dependencies: # Basic Python dependencies - python=3.11.0 - pip + - setuptools<82 # to avoid ImportErrors from pkg_resources deprecation # Unit testing - pytest diff --git a/devtools/conda-envs/test-env.yml b/devtools/conda-envs/test-env.yml index ff212e25..9fe1f7de 100644 --- a/devtools/conda-envs/test-env.yml +++ b/devtools/conda-envs/test-env.yml @@ -6,6 +6,7 @@ dependencies: # Basic Python dependencies - python=3.11.0 - pip + - setuptools<82 # to avoid ImportErrors from pkg_resources deprecation # Unit testing - pytest diff --git a/polymerist/genutils/fileutils/jsonio/serialize.py b/polymerist/genutils/fileutils/jsonio/serialize.py index 502392b3..146d1151 100644 --- a/polymerist/genutils/fileutils/jsonio/serialize.py +++ b/polymerist/genutils/fileutils/jsonio/serialize.py @@ -55,7 +55,6 @@ def decoder_hook(cls, json_dict : dict[JSONSerializable, JSONSerializable]) -> U if type_name is None: return json_dict # return unmodified dict for untyped entries - # raise Exception when attempting to decode typed entry of the wrong type if type_name != cls.python_type.__name__: raise TypeError(f'{cls.python_type.__name__} decoder cannot decode JSON-serialized object of type {type_name}') @@ -89,7 +88,7 @@ def encoder_default(self, python_obj : Any) -> JSONSerializable: for type_ser in self.type_sers: try: return type_ser.encoder_default(python_obj) - except: + except TypeError: pass # keep trying rest of encoders (don't immediately raise error) - TODO : make this less redundant-looking? else: raise TypeError(f'Object of type {python_obj.__class__.__name__} is not JSON serializable') @@ -98,13 +97,24 @@ def decoder_hook(self, json_dict : dict[JSONSerializable, JSONSerializable]) -> for type_ser in self.type_sers: try: return type_ser.decoder_hook(json_dict) - except: + except TypeError: pass # keep trying rest of encoders (don't immediately raise error) - TODO : make this less redundant-looking? else: # only raised if no return occurs in any iteration - each decoder works for default-decodable values raise TypeError(f'No registered decoders for dict : {json_dict}') # CONCRETE IMPLEMENTATIONS +S = TypeVar('S', bound=JSONSerializable) +class SetSerializer(TypeSerializer, python_type=set): + '''For JSON-serializing builtin Python sets''' + @staticmethod + def encode(python_obj : set[S]) -> list[S]: + return list(python_obj) + + @staticmethod + def decode(json_obj : list[S]) -> set[S]: + return set(json_obj) + class PathSerializer(TypeSerializer, python_type=Path): '''For JSON-serializing OpenMM Quantities''' @staticmethod @@ -149,7 +159,7 @@ def decode(json_obj : dict[str, Union[str, float]]) -> openmm.unit.Quantity: class NDArraySerializer(TypeSerializer, python_type=np.ndarray): '''For JSON-serializing of numpy n-dimensional arrays''' @staticmethod - def encode(python_obj : np.ndarray[Any]) -> list[Any]: + def encode(python_obj : np.ndarray) -> dict[str, Union[list, str]]: '''List-ify array and store string descriptor of numpy dtype''' return { 'array' : python_obj.tolist(), @@ -157,7 +167,7 @@ def encode(python_obj : np.ndarray[Any]) -> list[Any]: } @staticmethod - def decode(value : list[Any]) -> np.ndarray[Any]: + def decode(value : dict[str, Union[list, str]]) -> np.ndarray: '''Reassemble numpy array from list and dtype''' return np.array(value['array'], dtype=value['dtype']) diff --git a/polymerist/genutils/importutils/dependencies.py b/polymerist/genutils/importutils/dependencies.py index 2e555c29..8de97614 100644 --- a/polymerist/genutils/importutils/dependencies.py +++ b/polymerist/genutils/importutils/dependencies.py @@ -16,13 +16,14 @@ class MissingPrerequisitePackage(Exception): '''Raised when a package dependency cannot be found and the user should be alerted with install instructions''' - def __init__(self, - importing_package_name : str, - use_case : str, - install_link : str, - dependency_name : str, - dependency_name_formal : Optional[str]=None - ): + def __init__( + self, + importing_package_name : str, + use_case : str, + install_link : str, + dependency_name : str, + dependency_name_formal : Optional[str]=None + ): if dependency_name_formal is None: dependency_name_formal = dependency_name @@ -61,7 +62,7 @@ def module_installed(module_name : str) -> bool: except (ValueError, AttributeError, ModuleNotFoundError): # these could all be raised by a missing module return False -def modules_installed(*module_names : list[str]) -> bool: +def modules_installed(*module_names : str) -> bool: ''' Check whether one or more modules are all present Will only return true if ALL specified modules are found @@ -79,9 +80,9 @@ def modules_installed(*module_names : list[str]) -> bool: return all(module_installed(module_name) for module_name in module_names) def requires_modules( - *required_module_names : list[str], - missing_module_error : Union[Exception, type[Exception]]=ImportError, - ) -> Callable[[TCall[..., ReturnType]], TCall[..., ReturnType]]: + *required_module_names : str, + missing_module_error : Union[Exception, type[Exception]]=ImportError, +) -> Callable[[TCall[..., ReturnType]], TCall[..., ReturnType]]: ''' Decorator which enforces optional module dependencies prior to function execution diff --git a/polymerist/genutils/importutils/pkginspect.py b/polymerist/genutils/importutils/pkginspect.py index 34fd72e2..d8d6b29b 100644 --- a/polymerist/genutils/importutils/pkginspect.py +++ b/polymerist/genutils/importutils/pkginspect.py @@ -54,7 +54,7 @@ def module_parts(module : Union[str, ModuleType]) -> tuple[Optional[str], str]: return parent_package_name, module_stem -def module_stem(module : Union[str, ModuleType]) -> tuple[Optional[str], str]: +def module_stem(module : Union[str, ModuleType]) -> str: '''Takes a module (as its name or as ModuleType) and returns its relative module name''' return module_parts(module)[-1] diff --git a/polymerist/genutils/importutils/pkgiter.py b/polymerist/genutils/importutils/pkgiter.py index 2eff1d83..08487a8b 100644 --- a/polymerist/genutils/importutils/pkgiter.py +++ b/polymerist/genutils/importutils/pkgiter.py @@ -51,11 +51,12 @@ def children(self, module : ModuleType) -> Iterable[ModuleType]: # BACKWARDS-COMPATIBLE PORTS OF LEGACY IMPORTUTILS FUNCTIONS def module_tree_direct( - module : ModuleType, - recursive : bool=True, - blacklist : Optional[Container[str]]=None, - ) -> Node: - '''Produce a tree from the Python package hierarchy starting with a given module + module : ModuleType, + recursive : bool=True, + blacklist : Optional[Container[str]]=None, +) -> Node: + ''' + Produce a tree from the Python package hierarchy starting with a given module Parameters ---------- @@ -83,10 +84,10 @@ def module_tree_direct( ) def iter_submodules( - module : ModuleType, - recursive : bool=True, - blacklist : Optional[Container[str]]=None, - ) -> Generator[ModuleType, None, None]: + module : ModuleType, + recursive : bool=True, + blacklist : Optional[Container[str]]=None, +) -> Generator[ModuleType, None, None]: ''' Generates all modules which can be imported from the given toplevel module @@ -111,10 +112,10 @@ def iter_submodules( yield module_node.module def iter_submodule_info( - module : ModuleType, - recursive : bool=True, - blacklist : Optional[Container[str]]=None, - ) -> Generator[tuple[ModuleType, str, bool], None, None]: + module : ModuleType, + recursive : bool=True, + blacklist : Optional[Container[str]]=None, +) -> Generator[tuple[ModuleType, str, bool], None, None]: ''' Generates information about all modules which can be imported from the given toplevel module Namely, yields the module object, module name, and whether or not the module is a package @@ -141,10 +142,10 @@ def iter_submodule_info( yield module_node.module, module_node.name, module_node.is_leaf def register_submodules( - module : ModuleType, - recursive : bool=True, - blacklist : Optional[Container[str]]=None - ) -> None: + module : ModuleType, + recursive : bool=True, + blacklist : Optional[Container[str]]=None +) -> None: ''' Registers all submodules of a given module into it's own namespace (i.e. autoimports submodules) @@ -167,11 +168,11 @@ def register_submodules( setattr(module, submodule.__name__, submodule) def module_hierarchy( - module : ModuleType, - recursive : bool=True, - blacklist : Optional[Container[str]]=None, - style : Union[str, AbstractStyle]=ContStyle() - ) -> str: + module : ModuleType, + recursive : bool=True, + blacklist : Optional[Container[str]]=None, + style : Union[str, AbstractStyle]=ContStyle() +) -> str: ''' Generates a printable string which summarizes a Python packages hierarchy. Reminiscent of GNU tree output diff --git a/polymerist/genutils/trees/treebase.py b/polymerist/genutils/trees/treebase.py index 140b5a8e..fe13aeab 100644 --- a/polymerist/genutils/trees/treebase.py +++ b/polymerist/genutils/trees/treebase.py @@ -34,11 +34,11 @@ def children(self, obj : T) -> Optional[Iterable[T]]: pass def compile_tree_factory( - node_corresp : NodeCorrespondence[T], - class_alias : Optional[str]=None, - obj_attr_name : Optional[str]=None, - exclude_mixin : Optional[Filter[T]]=None, - ) -> Callable[[T, Optional[int], Optional[Filter[T]]], Node]: + node_corresp : NodeCorrespondence[T], + class_alias : Optional[str]=None, + obj_attr_name : Optional[str]=None, + exclude_mixin : Optional[Filter[T]]=None, +) -> Callable[[T, Optional[int], Optional[Filter[T]]], Node]: ''' Factory method for producing a tree-generating function from a NodeCorrespondence diff --git a/polymerist/mdtools/openmmtools/evaluation.py b/polymerist/mdtools/openmmtools/evaluation.py index a02e6158..546a32bd 100644 --- a/polymerist/mdtools/openmmtools/evaluation.py +++ b/polymerist/mdtools/openmmtools/evaluation.py @@ -19,10 +19,10 @@ def get_context_positions(context : Context) -> Quantity: # ENERGIES def get_openmm_energies( - context : Context, - preferred_unit : Optional[Unit]=None, - force_group_names : Optional[dict[int, str]]=None, - ) -> dict[str, Quantity]: + context : Context, + preferred_unit : Optional[Unit]=None, + force_group_names : Optional[dict[int, str]]=None, +) -> dict[str, Quantity]: ''' Evaluate energies of an OpenMM Context diff --git a/polymerist/mdtools/openmmtools/execution.py b/polymerist/mdtools/openmmtools/execution.py index c0620318..713a89e2 100644 --- a/polymerist/mdtools/openmmtools/execution.py +++ b/polymerist/mdtools/openmmtools/execution.py @@ -22,14 +22,14 @@ def run_simulation_schedule( - working_dir : Path, - schedule : dict[str, SimulationParameters], - init_top : Topology, - init_sys : System, - init_pos : ndarray, - init_state : Optional[StateLike]=None, - return_history : bool=False, - ) -> Optional[dict[str, tuple[Simulation, SimulationPaths]]]: + working_dir : Path, + schedule : dict[str, SimulationParameters], + init_top : Topology, + init_sys : System, + init_pos : ndarray, + init_state : Optional[StateLike]=None, + return_history : bool=False, +) -> Optional[dict[str, tuple[Simulation, SimulationPaths]]]: '''Run several OpenMM simulations in series, based on an initial set of OpenMM objects and a "schedule" consisting of a sequence of named parameter sets''' if not isinstance(init_pos, Quantity): raise TypeError('Positions must have associated OpenMM units') # TODO : provide more robust check for this diff --git a/polymerist/mdtools/openmmtools/preparation.py b/polymerist/mdtools/openmmtools/preparation.py index 52c91389..d3b66ac2 100644 --- a/polymerist/mdtools/openmmtools/preparation.py +++ b/polymerist/mdtools/openmmtools/preparation.py @@ -21,13 +21,13 @@ def simulation_from_thermo( - topology : Topology, - system : System, - thermo_params : ThermoParameters, - time_step : Quantity, - positions : Optional[Quantity]=None, - state : Optional[StateLike]=None, - ) -> Simulation: + topology : Topology, + system : System, + thermo_params : ThermoParameters, + time_step : Quantity, + positions : Optional[Quantity]=None, + state : Optional[StateLike]=None, +) -> Simulation: '''Prepare an OpenMM simulation from a serialized thermodynamics parameter set''' # clear forces added from another set of thermodynamic parameters to avoid thermostat/barostat "bleedover" preexisting_force_indices : list[int] = sorted( @@ -70,14 +70,14 @@ def simulation_from_thermo( return simulation def initialize_simulation_and_files( - out_dir : Path, - prefix : str, - sim_params : SimulationParameters, - topology : Topology, - system : System, - positions : Optional[Quantity]=None, - state : Optional[StateLike]=None, - ) -> tuple[Simulation, SimulationPaths]: + out_dir : Path, + prefix : str, + sim_params : SimulationParameters, + topology : Topology, + system : System, + positions : Optional[Quantity]=None, + state : Optional[StateLike]=None, +) -> tuple[Simulation, SimulationPaths]: '''Create simulation, bind Reporters, and update simulation Paths with newly-generated files''' sim_paths = SimulationPaths.from_dir_and_parameters(out_dir, prefix, sim_params, touch=True) simulation = simulation_from_thermo( diff --git a/polymerist/mdtools/openmmtools/serialization/paths.py b/polymerist/mdtools/openmmtools/serialization/paths.py index c6e4534c..c637ae4f 100644 --- a/polymerist/mdtools/openmmtools/serialization/paths.py +++ b/polymerist/mdtools/openmmtools/serialization/paths.py @@ -33,11 +33,11 @@ class SimulationPaths: @allow_string_paths def init_top_and_sys_paths( - self, - out_dir : Path, - prefix : str, - record : bool=True, - ) -> tuple[Path, Path]: + self, + out_dir : Path, + prefix : str, + record : bool=True, + ) -> tuple[Path, Path]: '''Initialize Topology and System output paths for a given directory''' topology_path = assemble_path(out_dir, prefix, extension='pdb', postfix='topology') system_path = assemble_path(out_dir, prefix, extension='xml', postfix='system') @@ -52,12 +52,12 @@ def init_top_and_sys_paths( @classmethod def from_dir_and_parameters( - cls, - out_dir : Path, - prefix : str, - sim_params : SimulationParameters, - touch : bool=True, - ) -> 'SimulationPaths': + cls, + out_dir : Path, + prefix : str, + sim_params : SimulationParameters, + touch : bool=True, + ) -> 'SimulationPaths': '''Create file directory and initialize simulationPaths object from a set of SimulationParameters''' path_obj = cls() # create empty path instance diff --git a/polymerist/mdtools/openmmtools/serialization/state.py b/polymerist/mdtools/openmmtools/serialization/state.py index b5b5dde5..9c0e643e 100644 --- a/polymerist/mdtools/openmmtools/serialization/state.py +++ b/polymerist/mdtools/openmmtools/serialization/state.py @@ -53,10 +53,10 @@ def load_state_flexible(state : Optional[StateLike]=None) -> Optional[State]: @allow_string_paths def serialize_state_from_context( - state_path : Path, - context : Context, - state_params : dict[str, bool]=None, - ) -> None: + state_path : Path, + context : Context, + state_params : dict[str, bool]=None, +) -> None: '''For saving State data within an existing OpenMM Context to file''' if state_params is None: state_params = DEFAULT_STATE_PROPS diff --git a/polymerist/mdtools/openmmtools/serialization/topology.py b/polymerist/mdtools/openmmtools/serialization/topology.py index 8112c447..471ea9c4 100644 --- a/polymerist/mdtools/openmmtools/serialization/topology.py +++ b/polymerist/mdtools/openmmtools/serialization/topology.py @@ -22,13 +22,13 @@ @allow_string_paths def serialize_openmm_pdb( - pdb_path : Path, - topology : OpenMMTopology, - positions : Union[NDArray, list[Vec3]], - keep_chain_and_res_ids : bool=True, - atom_labeller : Optional[SerialAtomLabeller]=None, - resname_map : Optional[dict[str, str]]=None, - ) -> None: + pdb_path : Path, + topology : OpenMMTopology, + positions : Union[NDArray, list[Vec3]], + keep_chain_and_res_ids : bool=True, + atom_labeller : Optional[SerialAtomLabeller]=None, + resname_map : Optional[dict[str, str]]=None, +) -> None: '''Configure and write an Protein DataBank File from an OpenMM Topology and array of positions Provides options to configure atom ID numbering, residue numbering, and residue naming''' if atom_labeller is None: diff --git a/polymerist/polymers/building/linear.py b/polymerist/polymers/building/linear.py index a7c47e0e..955ae368 100644 --- a/polymerist/polymers/building/linear.py +++ b/polymerist/polymers/building/linear.py @@ -11,6 +11,8 @@ warnings.filterwarnings('ignore', category=DeprecationWarning) from mbuild.lib.recipes.polymer import Polymer as MBPolymer +from typing import Optional + from .mbconvert import mbmol_from_mono_rdmol from .sequencing import LinearCopolymerSequencer from ..exceptions import MorphologyError @@ -20,54 +22,94 @@ def build_linear_polymer( - monomers : MonomerGroup, - n_monomers : int, - sequence : str='A', - minimize_sequence : bool=True, - allow_partial_sequences : bool=False, - add_Hs : bool=False, - energy_minimize : bool=False, - ) -> MBPolymer: + monomers : MonomerGroup, + n_monomers : int, + sequence : str='A', + minimize_sequence : bool=True, + allow_partial_sequences : bool=False, + add_Hs : bool=False, + energy_minimize : bool=False, + sequence_map : Optional[dict[str, str]]=None, +) -> MBPolymer: ''' Builds a linear polymer structure from a specified pool of monomers, sequence, target chain length, and other parameters Parameters ---------- monomers : MonomerGroup - A group of fragments containing at least the distinct repeat units which occur in the target polymer + A group of fragments containing AT LEAST the distinct repeat units which occur in the target polymer IMPORTANT: if the "term_orient" field of the MonomerGroup is not set (with "head" and "tail" monomer designations), the first two terminal (1-valent) monomers in the group will be auto-assigned and taken as the head and tail, respectively, or, if there is only one terminal monomer present, it will be used as both the head and tail. n_monomers : int The number of monomer units in the target polymer chain - This includes the terminal monomers in the count, e.g. n_monomers=10 with a head and tail group specified will induce 8 middle monomers - sequence : str, default='A' - A string of characters representing the sequence of monomers as they should occur within the polymer chain - Each unique character in the string will be associated with a unique monomer in the provided MonomerGroup, - in the order that they appear, e.g. "BACA" will take the second, first, third, and first monomers defined in the group - IMPORTANT: the sequence string only specifies the MIDDLE monomers in the chain, i.e. terminal monomers are not given by the sequence string, - but either by the "term_orient" field of the MonomerGroup or the auto-determined end groups if that is unset + This INCLUDES the terminal monomers in the count; + E.g. n_monomers=10 with a head and tail group specified will induce 8 middle monomers + sequence : str, default='A' + A string of characters representing the sequence of MIDDLE monomers as they should occur within the polymer chain + If the sequence is shorter than n_monomers - # end groups, the sequence will be repeated until the target chain length is reached. + + Each unique character in the string will be associated with a unique repeat unit + in the provided MonomerGroup, as specified by `sequence_map` (see below) - If the sequence is shorter than n_monomers, the sequence will be repeated until the target chain length is reached. + IMPORTANT: terminal monomers are not given by this sequence string, but are specified by either + the "term_orient" field of the MonomerGroup or the auto-determined end groups if that is unset minimize_sequence : bool, default=True - Whether to attempt to reduce the sequence provided into a minimal, repeating subsequence + Whether to attempt to reduce the sequence provided into a minimal, repeating subsequence ("kernel") E.g. "ABABAB" will be reduced to 3*"AB" if this is set to True - Note carefully that this has NOTHING TO DO WITH energy minimization; that is controlled by the energy_minimize flag + N.B.: this has NOTHING TO DO WITH energy minimization; that is controlled by the energy_minimize flag allow_partial_sequences : bool, default=False Whether to allow fractional repetitions of the sequence kernel to fill the target chain length - For example, given a monomer group with head/tail specified and parameters n_monomers=10 and sequence="BAC" (inducing 10 - 2 = 8 middle monomers): - allow_partial_sequences=True will repeat the sequence 2 + 2/3 times, yielding the equivalent middle monomer sequence "BACBACBA", while - allow_partial_sequences=False would raise Exception, since the sequence "BAC" cannot be repeated to fill 8 middle monomers exactly. + For example, given a monomer group with head/tail specified and parameters + n_monomers=10 and sequence="BAC" (inducing 10 - 2 = 8 middle monomers): + * allow_partial_sequences=True will repeat the sequence 2 + 2/3 times, + yielding the equivalent middle monomer sequence "BACBACBA", while + * allow_partial_sequences=False would raise PartialBlockSequence Exception, + since the sequence "BAC" cannot be repeated to fill 8 middle monomers exactly. add_Hs : bool, default=False Whether to instruct the mbuild Polymer recipe to cap uncapped terminal groups with hydrogens, in cases where the user has failed to provide ANY terminal monomers in the MonomerGroup energy_minimize : bool, default=False Whether to perform a brief UFF energy minimization after build to relax the resulting polymer structure - Tends to give less-unphysical conformers for larger polymers, but is significantly slower, especially for longer chains + Tends to give less-unphysical conformers for larger polymers but is significantly slower, especially for longer chains + sequence_map : dict[str, str], default None + Mapping from individual symbols (characters) in the sequence to the + names of repeat units in monomers to associate with those symbols + + If no explicit mapping is provided, each unique symbol will be mapped to the + MIDDLE repeat unit at that symbol's position when lexicographically sorted + E.g. "BACA" will take the second, first, third, and first monomers defined + in the MonomerGroup; so will "baca", "2131", "caea", and "tipi" + + For a more detailed example, suppose the contents of monomers.monomers looked like: + >>> { + >>> 'EG_term' : [...] + >>> 'EG' : [...], + >>> 'GA' : [...], + >>> 'LA' : [...], + >>> ... + >>> } + + Given sequence='bac' with sequence_map unset, a map of + >>> sequence_map = { # this is the default!! + >>> 'a' : 'EG', + >>> 'b' : 'GA', + >>> 'c' : 'LA', + >>> } + would be generated by default (skipping over the terminal repeat unit 'EG_term') and + the resulting polymer would be build as [head group]-[GA]-[EG]-[LA]-[GA]-[EG]-... + + On the other hand, supplying a map as: + >>> sequence_map = { + >>> 'a' : 'GA', + >>> 'b' : 'LA', + >>> 'c' : 'EG', + >>> } + would instead assemble the polymer as [head group]-[LA]-[GA]-[EG]-[LA]-[GA]-... Returns ------- @@ -93,12 +135,36 @@ def build_linear_polymer( ) sequence_unique = unique_string(sequence_compliant, preserve_order=True) # only register a new monomer for each appearance of a new, unique symbol in the sequence - # 2) REGISTERING MONOMERS TO BE USED FOR CHAIN ASSEMBLY + # 2) REGISTER MONOMERS TO BE USED FOR CHAIN ASSEMBLY polymer = MBPolymer() - monomers_selected = MonomerGroup() # used to track and estimate sized of the monomers being used for building + monomers_selected = MonomerGroup() # need only the subset of repeat units used in assembly to accurately assess linearity and estimate chain size (in current implementations) ## 2A) ADD MIDDLE MONOMERS TO CHAIN - for symbol, (resname, middle_monomer) in zip(sequence_unique, monomers.iter_rdmols(term_only=False)): # zip with sequence limits number of middle monomers to length of block sequence + if sequence_map is None: + sequence_map = { + symbol : resname + for symbol, (resname, middle_monomer) in zip( + sorted(sequence_unique), + monomers.iter_rdmols(term_only=False), # iterate over non-terminal repeat units only + ) + } + + middle_monomer_table = monomers.rdmols(term_only=False) # cache this locally - DEV: ugly, but don't want to shift MonomerGroup API around too much + if (num_symbols_unique := len(sequence_unique)) > (num_middle_monomers := len(middle_monomer_table)): + raise ValueError(f'Too few unique repeat units ({num_middle_monomers}) to ascribe to each symbols of a {num_symbols_unique}-symbol sequence') + + for symbol in sorted(sequence_unique): # avoiding sequence_map.items() - won't count on sequence map being sorted (e.g. if a user passes one in) + resname = sequence_map[symbol] # another reason not to use sequence_map.items() here is we want a big, fat KeyError for undefined symbols in the sequence + middle_monomer_choices = middle_monomer_table[resname] + num_middle_monomer_choices : int = len(middle_monomer_choices) + + if num_middle_monomer_choices == 0: + raise IndexError(f'No monomer templates for "{resname}" defined in MonomerGroup') + elif num_middle_monomer_choices > 1: + raise IndexError(f'Ambiguous choice for template "{resname}" ({num_middle_monomer_choices} templates defined for that label)') + else: + middle_monomer = middle_monomer_table[resname][0] + LOGGER.info(f'Registering middle monomer {resname} (block identifier "{symbol}")') mb_monomer, linker_ids = mbmol_from_mono_rdmol(middle_monomer, resname=resname) polymer.add_monomer(compound=mb_monomer, indices=linker_ids) diff --git a/polymerist/polymers/building/mbconvert.py b/polymerist/polymers/building/mbconvert.py index 3facbdd0..965f57c9 100644 --- a/polymerist/polymers/building/mbconvert.py +++ b/polymerist/polymers/building/mbconvert.py @@ -35,7 +35,11 @@ # Conversion from other formats to Compound -def mbmol_from_mono_rdmol(rdmol : Chem.Mol, resname : Optional[str]=None, kekulize : bool=True) -> tuple[Compound, list[int]]: +def mbmol_from_mono_rdmol( + rdmol : Chem.Mol, + resname : Optional[str]=None, + kekulize : bool=True, +) -> tuple[Compound, list[int]]: ''' Accepts a monomer-spec-compliant SMARTS string and returns an mbuild Compound and a list of the indices of atom ports If "resname" is provided, will assign that name to the mBuild Compound returned @@ -67,10 +71,10 @@ def mbmol_from_mono_rdmol(rdmol : Chem.Mol, resname : Optional[str]=None, kekuli } def mbmol_to_rdmol( # TODO: deduplify PDB atom name and residue numbering code against serialize_openmm_pdb() - mbmol : Compound, - atom_labeller : Optional[SerialAtomLabeller]=None, - resname_map : Optional[dict[str, str]]=None - ) -> Chem.Mol: + mbmol : Compound, + atom_labeller : Optional[SerialAtomLabeller]=None, + resname_map : Optional[dict[str, str]]=None +) -> Chem.Mol: '''Convert an mBuild Compound into an RDKit Mol, with correct atom coordinates and PDB residue info''' if atom_labeller is None: atom_labeller = SerialAtomLabeller() @@ -111,11 +115,11 @@ def mbmol_to_rdmol( # TODO: deduplify PDB atom name and residue numbering code a # Serialization of Compounds to files @allow_pathlib_paths def mbmol_to_rdkit_pdb( - pdb_path : str, - mbmol : Compound, - atom_labeller : Optional[SerialAtomLabeller]=None, - resname_map : Optional[dict[str, str]]=None, - ) -> None: + pdb_path : str, + mbmol : Compound, + atom_labeller : Optional[SerialAtomLabeller]=None, + resname_map : Optional[dict[str, str]]=None, +) -> None: # DEV: "missing" docstring here is deliberate; this is needed to dynamically set the resname_map default as it displays Chem.MolToPDBFile( mbmol_to_rdmol( @@ -148,11 +152,11 @@ def mbmol_to_rdkit_pdb( @allow_string_paths def mbmol_to_openmm_pdb( - pdb_path : Path, - mbmol : Compound, - atom_labeller : Optional[SerialAtomLabeller]=None, - resname_map : Optional[dict[str, str]]=None, - ) -> None: + pdb_path : Path, + mbmol : Compound, + atom_labeller : Optional[SerialAtomLabeller]=None, + resname_map : Optional[dict[str, str]]=None, +) -> None: # DEV: "missing" docstring here is deliberate; this is needed to dynamically set the resname_map default as it displays if resname_map is None: # avoid mutable default resname_map = _DEFAULT_RESNAME_MAP diff --git a/polymerist/polymers/monomers/fragments.py b/polymerist/polymers/monomers/fragments.py index a734e80a..bd32693b 100644 --- a/polymerist/polymers/monomers/fragments.py +++ b/polymerist/polymers/monomers/fragments.py @@ -29,4 +29,22 @@ 'PGA-1A': ['[#8D2+0:1](-[#6D4+0:2](-[#6D3+0:3](=[#8D1+0:4])-[*:5])(-[#1D1+0:7])-[#1D1+0:8])-[#1D1+0:6]'], 'PGA-1B': ['[*:1]-[#8D2+0:2]-[#6D4+0:3](-[#6D3+0:4](=[#8D1+0:5])-[#8D2+0:6]-[#1D1+0:9])(-[#1D1+0:7])-[#1D1+0:8]'], 'PGA-2': ['[*:1]-[#8D2+0:2]-[#6D4+0:3](-[#6D3+0:4](=[#8D1+0:5])-[*:6])(-[#1D1+0:7])-[#1D1+0:8]'], +} + +HALOGENATED_HYDROCARBON_FRAGMENTS = { # halogenated hydrocarbons with ester tails - particularly easy to visually identify sequences + # fluorides + 'fluor_term_1': ['[#8D2+0:1](-[#6D4+0:2](-[#9D1+0:3])(-[*:4])-[#1D1+0:6])-[#1D1+0:5]'], # oxygen end group with 1 halogen on the carbon + 'fluor_mid_1': ['[*:1]-[#6D4+0:2](-[#9D1+0:3])(-[*:4])-[#1D1+0:5]'], # singly-halogenated carbon + 'fluor_mid_2': ['[*:1]-[#6D4+0:2](-[#9D1+0:3])(-[#9D1+0:4])-[*:5]'], # doubly-halogenated carbon + 'fluor_term_2': ['[*:1]-[#6D4+0:2](-[#9D1+0:3])(-[#6D3+0:4](=[#8D1+0:5])-[#8D2+0:6]-[#1D1+0:8])-[#1D1+0:7]'], # carbxyl end group with singly-halogenated carbon + # chlorides + 'chlor_term_1': ['[#8D2+0:1](-[#6D4+0:2](-[#17D1+0:3])(-[*:4])-[#1D1+0:6])-[#1D1+0:5]'], # same pattern as for fluorides + 'chlor_mid_1': ['[*:1]-[#6D4+0:2](-[#17D1+0:3])(-[*:4])-[#1D1+0:5]'], + 'chlor_mid_2': ['[*:1]-[#6D4+0:2](-[#17D1+0:3])(-[#17D1+0:4])-[*:5]'], + 'chlor_term_2': ['[*:1]-[#6D4+0:2](-[#17D1+0:3])(-[#6D3+0:4](=[#8D1+0:5])-[#8D2+0:6]-[#1D1+0:8])-[#1D1+0:7]'], + # bromides + 'brom_term_1': ['[#8D2+0:1](-[#6D4+0:2](-[#35D1+0:3])(-[*:4])-[#1D1+0:6])-[#1D1+0:5]'], + 'brom_mid_1': ['[*:1]-[#6D4+0:2](-[#35D1+0:3])(-[*:4])-[#1D1+0:5]'], + 'brom_mid_2': ['[*:1]-[#6D4+0:2](-[#35D1+0:3])(-[#35D1+0:4])-[*:5]'], + 'brom_term_2': ['[*:1]-[#6D4+0:2](-[#35D1+0:3])(-[#6D3+0:4](=[#8D1+0:5])-[#8D2+0:6]-[#1D1+0:8])-[#1D1+0:7]'], } \ No newline at end of file diff --git a/polymerist/polymers/monomers/repr.py b/polymerist/polymers/monomers/repr.py index 945bce6c..19d54332 100644 --- a/polymerist/polymers/monomers/repr.py +++ b/polymerist/polymers/monomers/repr.py @@ -6,7 +6,7 @@ import logging LOGGER = logging.getLogger(__name__) -from typing import Generator, Optional, Iterable, Union +from typing import Generator, Iterable, Mapping, Optional, Union from dataclasses import dataclass, field from itertools import cycle @@ -25,7 +25,7 @@ @dataclass class MonomerGroup: '''Stores collections of residue-labelled monomer SMARTS''' - monomers : dict[str, Union[Smarts, list[Smarts]]] = field(default_factory=dict) + monomers : dict[str, list[Smarts]] = field(default_factory=dict) term_orient : dict[str, str] = field(default_factory=dict) # keys are either "head" or "tail", values are the names of residues in "monomers" # MONOMER ADDITION AND VALIDATION @@ -68,13 +68,13 @@ def add_monomer(self, resname : str, smarts : Union[Smarts, Iterable[Smarts]]) - self._add_monomer(resname, smarts) # assume any other inputs are singular values or strings # DUNDER "MAGIC" METHODS - def __getitem__(self, resname : str) -> str: + def __getitem__(self, resname : str) -> list[Smarts]: '''Convenience method to access .monomers directly from instance''' return self.monomers[resname] # NOTE: deliberately avoid "get()" here to propagate KeyError # BUG: user can directly append to the returned value to forgo monomer validation checks; - # this is not unit to __getitem__ but rather a consequence of thinly-wrapping builtin types + # this is not unique to __getitem__ but rather a consequence of thinly-wrapping builtin types - def __setitem__(self, resname : str, smarts : Smarts) -> str: + def __setitem__(self, resname : str, smarts : Smarts) -> None: '''Convenience method to access .monomers directly from instance''' self.add_monomer(resname, smarts) @@ -140,7 +140,7 @@ def n_monomers(self) -> int: return sum(1 for _ in self.iter_rdmols(term_only=None)) # END GROUP DETERMINATION - def linear_end_groups(self) -> dict[str, tuple[str, Chem.Mol]]: + def linear_end_groups(self) -> Mapping[str, tuple[str, Chem.Mol]]: ''' Returns head-and-tail end group residue names and Mol objects as defined by term_orient @@ -166,7 +166,7 @@ def linear_end_groups(self) -> dict[str, tuple[str, Chem.Mol]]: } else: term_orient_auto : dict[str, Smarts] = {} - end_groups_auto : dict[str, Chem.Mol] = {} + end_groups_auto : dict[str, tuple[str, Chem.Mol]] = {} for head_or_tail, (resname, rdmol) in zip(['head', 'tail'], self.iter_rdmols(term_only=True)): # zip will bottom out early if fewer than 2 terminal monomers are present term_orient_auto[head_or_tail] = resname # populate purely for logging end_groups_auto[head_or_tail] = (resname, rdmol) @@ -186,6 +186,7 @@ def __add__(self, other : 'MonomerGroup') -> 'MonomerGroup': __radd__ = __add__ # support reverse addition # CHEMICAL INFORMATION + @property def is_homopolymer(self) -> bool: '''Identify if a polymer is a homopolymer (i.e. only 1 type of middle monomer)''' return (len(self.rdmols(term_only=False)) == 1) # by definition, a homopolymer only has 1 unique class of middle monomer diff --git a/polymerist/rdutils/rdcoords/tiling.py b/polymerist/rdutils/rdcoords/tiling.py index 421570c9..03085d40 100644 --- a/polymerist/rdutils/rdcoords/tiling.py +++ b/polymerist/rdutils/rdcoords/tiling.py @@ -3,6 +3,9 @@ __author__ = 'Timotej Bernat' __email__ = 'timotej.bernat@colorado.edu' +import logging +LOGGER = logging.getLogger(__name__) + import numpy as np from rdkit.Chem import Mol, CombineMols from rdkit.Chem.rdMolTransforms import ComputeCanonicalTransform, TransformConformer @@ -31,6 +34,15 @@ def tile_lattice_with_rdmol(rdmol : Mol, lattice_points : np.ndarray[Shape[N, 3] conformer = rdmol.GetConformer(conf_id) centering = ComputeCanonicalTransform(conformer, ignoreHs=True) # translation which centers the given conformer + + rotation = centering[:-1, :-1] + orient_sign = np.sign(np.linalg.det(rotation)) + if orient_sign < 0.0: + centering[:-1, :-1] *= orient_sign # enforce right-handed coordinate system to avoid unintended inversions + LOGGER.warning( + 'Caught and corrected stereochemical inversion during RDKit alignment transform;\n' + 'recommend upgrading RDKit to >=2025.09.x(see https://github.com/rdkit/rdkit/issues/8720)' + ) tiled_topology = None for point in lattice_points: diff --git a/polymerist/rdutils/reactions/reactions.py b/polymerist/rdutils/reactions/reactions.py index be343029..ff8eb718 100644 --- a/polymerist/rdutils/reactions/reactions.py +++ b/polymerist/rdutils/reactions/reactions.py @@ -307,13 +307,13 @@ def compile_functional_group_inventory(self, labeled_reactants : dict[L, Mol]) - return SymbolInventory(fn_group_sym_inv) def enumerate_valid_reactant_orderings( - self, - reactant_pool : Union[Iterable[Mol], Mapping[L, Mol]], - labeling_method : Optional[Callable[[Mol], L]]=None, - as_mols : bool=True, - allow_resampling : bool=False, - deterministic : bool=True, - ) -> Generator[Union[None, tuple[L], tuple[Mol]], None, None]: + self, + reactant_pool : Union[Iterable[Mol], Mapping[L, Mol]], + labeling_method : Optional[Callable[[Mol], L]]=None, + as_mols : bool=True, + allow_resampling : bool=False, + deterministic : bool=True, + ) -> Generator[Union[None, tuple[L], tuple[Mol]], None, None]: ''' Enumerates all orderings of reactants compatible with the reactant templates defined in this reaction @@ -360,12 +360,12 @@ def enumerate_valid_reactant_orderings( yield None def valid_reactant_ordering( - self, - reactant_pool : Sequence[Mol], - as_mols : bool=True, - allow_resampling : bool=False, - deterministic : bool=True, - ) -> Union[None, tuple[int], tuple[Mol]]: + self, + reactant_pool : Sequence[Mol], + as_mols : bool=True, + allow_resampling : bool=False, + deterministic : bool=True, + ) -> Union[None, tuple[int], tuple[Mol]]: ''' Get first ordering of reactants compatible with the reactant templates defined in this reaction @@ -396,6 +396,14 @@ def has_reactable_subset(self, reactant_pool : Sequence[Mol], allow_resampling : deterministic=True, ) is not None + def acts_by_autopolymerization(self, reactant_pool : Sequence[Mol]) -> bool: + ''' + Determine whether this reaction necessarily acts via autopolymerization + (i.e. REQUIRES reactants to react with themselves to occur) on the given reactant inputs + ''' + return self.has_reactable_subset(reactant_pool, allow_resampling=True) \ + and not self.has_reactable_subset(reactant_pool, allow_resampling=False) # check if reaction becomes impossible if resampling of reactants is disallowed + # RUNNING CHEMICAL REACTIONS def validate_reactants(self, reactants : Sequence[Mol], allow_resampling : bool=False) -> None: '''Check whether a collection of reactant Mols can be reacted with this reaction definition''' @@ -418,11 +426,11 @@ def reactants_are_compatible(self, reactants : Sequence[Mol], allow_resampling : @staticmethod def apply_atom_info_to_product( - product : Mol, - product_atom_infos : Iterable['AtomTraceInfo'], - reactants : Sequence[Mol], - apply_map_labels : bool=True, - ) -> None: + product : Mol, + product_atom_infos : Iterable['AtomTraceInfo'], + reactants : Sequence[Mol], + apply_map_labels : bool=True, + ) -> None: '''Transfer props and (if requested) map number information from atoms in reactant Mols to their corresponding atoms in a product Mol Acts in-place on the "product" Mol instance''' for atom_info in product_atom_infos: @@ -439,9 +447,9 @@ def apply_atom_info_to_product( @staticmethod def apply_bond_info_to_product( - product : Mol, - product_bond_infos : Iterable['BondTraceInfo'], - ) -> None: + product : Mol, + product_bond_infos : Iterable['BondTraceInfo'], + ) -> None: '''Mark any changed bonds with bond props and clean up bond type info in places where bonds get modified Acts in-place on the "product" Mol instance''' for bond_info in product_bond_infos: @@ -460,12 +468,12 @@ def apply_bond_info_to_product( @sanitizable_mol_outputs def react( - self, - reactants : Sequence[Mol], - repetitions : int=1, - keep_map_labels : bool=True, - _suppress_reactant_validation : bool=False, - ) -> Generator[Mol, None, None]: + self, + reactants : Sequence[Mol], + repetitions : int=1, + keep_map_labels : bool=True, + _suppress_reactant_validation : bool=False, + ) -> Generator[Mol, None, None]: ''' Execute reaction over a collection of reactants and generate product molecule(s) Does not require reactants to match the ORDER of the expected reactant templates by default diff --git a/polymerist/tests/data/correct_discernment_solution.json b/polymerist/tests/data/correct_discernment_solution.json deleted file mode 100644 index d9db9f4d..00000000 --- a/polymerist/tests/data/correct_discernment_solution.json +++ /dev/null @@ -1,1010 +0,0 @@ -[ - [ - 1, - 0, - 1, - 5 - ], - [ - 1, - 0, - 1, - 7 - ], - [ - 1, - 0, - 4, - 5 - ], - [ - 1, - 0, - 4, - 7 - ], - [ - 1, - 0, - 6, - 5 - ], - [ - 1, - 0, - 6, - 7 - ], - [ - 1, - 0, - 7, - 5 - ], - [ - 1, - 0, - 7, - 7 - ], - [ - 1, - 1, - 0, - 5 - ], - [ - 1, - 1, - 0, - 7 - ], - [ - 1, - 1, - 4, - 5 - ], - [ - 1, - 1, - 4, - 7 - ], - [ - 1, - 1, - 6, - 5 - ], - [ - 1, - 1, - 6, - 7 - ], - [ - 1, - 1, - 7, - 5 - ], - [ - 1, - 1, - 7, - 7 - ], - [ - 1, - 4, - 0, - 5 - ], - [ - 1, - 4, - 0, - 7 - ], - [ - 1, - 4, - 1, - 5 - ], - [ - 1, - 4, - 1, - 7 - ], - [ - 1, - 4, - 4, - 5 - ], - [ - 1, - 4, - 4, - 7 - ], - [ - 1, - 4, - 6, - 5 - ], - [ - 1, - 4, - 6, - 7 - ], - [ - 1, - 4, - 7, - 5 - ], - [ - 1, - 4, - 7, - 7 - ], - [ - 1, - 6, - 0, - 5 - ], - [ - 1, - 6, - 0, - 7 - ], - [ - 1, - 6, - 1, - 5 - ], - [ - 1, - 6, - 1, - 7 - ], - [ - 1, - 6, - 4, - 5 - ], - [ - 1, - 6, - 4, - 7 - ], - [ - 1, - 6, - 7, - 5 - ], - [ - 1, - 6, - 7, - 7 - ], - [ - 1, - 7, - 0, - 5 - ], - [ - 1, - 7, - 0, - 7 - ], - [ - 1, - 7, - 1, - 5 - ], - [ - 1, - 7, - 1, - 7 - ], - [ - 1, - 7, - 4, - 5 - ], - [ - 1, - 7, - 4, - 7 - ], - [ - 1, - 7, - 6, - 5 - ], - [ - 1, - 7, - 6, - 7 - ], - [ - 2, - 0, - 1, - 5 - ], - [ - 2, - 0, - 1, - 7 - ], - [ - 2, - 0, - 4, - 5 - ], - [ - 2, - 0, - 4, - 7 - ], - [ - 2, - 0, - 6, - 5 - ], - [ - 2, - 0, - 6, - 7 - ], - [ - 2, - 0, - 7, - 5 - ], - [ - 2, - 0, - 7, - 7 - ], - [ - 2, - 1, - 0, - 5 - ], - [ - 2, - 1, - 0, - 7 - ], - [ - 2, - 1, - 4, - 5 - ], - [ - 2, - 1, - 4, - 7 - ], - [ - 2, - 1, - 6, - 5 - ], - [ - 2, - 1, - 6, - 7 - ], - [ - 2, - 1, - 7, - 5 - ], - [ - 2, - 1, - 7, - 7 - ], - [ - 2, - 4, - 0, - 5 - ], - [ - 2, - 4, - 0, - 7 - ], - [ - 2, - 4, - 1, - 5 - ], - [ - 2, - 4, - 1, - 7 - ], - [ - 2, - 4, - 4, - 5 - ], - [ - 2, - 4, - 4, - 7 - ], - [ - 2, - 4, - 6, - 5 - ], - [ - 2, - 4, - 6, - 7 - ], - [ - 2, - 4, - 7, - 5 - ], - [ - 2, - 4, - 7, - 7 - ], - [ - 2, - 6, - 0, - 5 - ], - [ - 2, - 6, - 0, - 7 - ], - [ - 2, - 6, - 1, - 5 - ], - [ - 2, - 6, - 1, - 7 - ], - [ - 2, - 6, - 4, - 5 - ], - [ - 2, - 6, - 4, - 7 - ], - [ - 2, - 6, - 7, - 5 - ], - [ - 2, - 6, - 7, - 7 - ], - [ - 2, - 7, - 0, - 5 - ], - [ - 2, - 7, - 0, - 7 - ], - [ - 2, - 7, - 1, - 5 - ], - [ - 2, - 7, - 1, - 7 - ], - [ - 2, - 7, - 4, - 5 - ], - [ - 2, - 7, - 4, - 7 - ], - [ - 2, - 7, - 6, - 5 - ], - [ - 2, - 7, - 6, - 7 - ], - [ - 3, - 0, - 1, - 5 - ], - [ - 3, - 0, - 1, - 7 - ], - [ - 3, - 0, - 4, - 5 - ], - [ - 3, - 0, - 4, - 7 - ], - [ - 3, - 0, - 6, - 5 - ], - [ - 3, - 0, - 6, - 7 - ], - [ - 3, - 0, - 7, - 5 - ], - [ - 3, - 0, - 7, - 7 - ], - [ - 3, - 1, - 0, - 5 - ], - [ - 3, - 1, - 0, - 7 - ], - [ - 3, - 1, - 4, - 5 - ], - [ - 3, - 1, - 4, - 7 - ], - [ - 3, - 1, - 6, - 5 - ], - [ - 3, - 1, - 6, - 7 - ], - [ - 3, - 1, - 7, - 5 - ], - [ - 3, - 1, - 7, - 7 - ], - [ - 3, - 4, - 0, - 5 - ], - [ - 3, - 4, - 0, - 7 - ], - [ - 3, - 4, - 1, - 5 - ], - [ - 3, - 4, - 1, - 7 - ], - [ - 3, - 4, - 4, - 5 - ], - [ - 3, - 4, - 4, - 7 - ], - [ - 3, - 4, - 6, - 5 - ], - [ - 3, - 4, - 6, - 7 - ], - [ - 3, - 4, - 7, - 5 - ], - [ - 3, - 4, - 7, - 7 - ], - [ - 3, - 6, - 0, - 5 - ], - [ - 3, - 6, - 0, - 7 - ], - [ - 3, - 6, - 1, - 5 - ], - [ - 3, - 6, - 1, - 7 - ], - [ - 3, - 6, - 4, - 5 - ], - [ - 3, - 6, - 4, - 7 - ], - [ - 3, - 6, - 7, - 5 - ], - [ - 3, - 6, - 7, - 7 - ], - [ - 3, - 7, - 0, - 5 - ], - [ - 3, - 7, - 0, - 7 - ], - [ - 3, - 7, - 1, - 5 - ], - [ - 3, - 7, - 1, - 7 - ], - [ - 3, - 7, - 4, - 5 - ], - [ - 3, - 7, - 4, - 7 - ], - [ - 3, - 7, - 6, - 5 - ], - [ - 3, - 7, - 6, - 7 - ], - [ - 6, - 0, - 1, - 5 - ], - [ - 6, - 0, - 1, - 7 - ], - [ - 6, - 0, - 4, - 5 - ], - [ - 6, - 0, - 4, - 7 - ], - [ - 6, - 0, - 6, - 5 - ], - [ - 6, - 0, - 6, - 7 - ], - [ - 6, - 0, - 7, - 5 - ], - [ - 6, - 0, - 7, - 7 - ], - [ - 6, - 1, - 0, - 5 - ], - [ - 6, - 1, - 0, - 7 - ], - [ - 6, - 1, - 4, - 5 - ], - [ - 6, - 1, - 4, - 7 - ], - [ - 6, - 1, - 6, - 5 - ], - [ - 6, - 1, - 6, - 7 - ], - [ - 6, - 1, - 7, - 5 - ], - [ - 6, - 1, - 7, - 7 - ], - [ - 6, - 4, - 0, - 5 - ], - [ - 6, - 4, - 0, - 7 - ], - [ - 6, - 4, - 1, - 5 - ], - [ - 6, - 4, - 1, - 7 - ], - [ - 6, - 4, - 4, - 5 - ], - [ - 6, - 4, - 4, - 7 - ], - [ - 6, - 4, - 6, - 5 - ], - [ - 6, - 4, - 6, - 7 - ], - [ - 6, - 4, - 7, - 5 - ], - [ - 6, - 4, - 7, - 7 - ], - [ - 6, - 6, - 0, - 5 - ], - [ - 6, - 6, - 0, - 7 - ], - [ - 6, - 6, - 1, - 5 - ], - [ - 6, - 6, - 1, - 7 - ], - [ - 6, - 6, - 4, - 5 - ], - [ - 6, - 6, - 4, - 7 - ], - [ - 6, - 6, - 7, - 5 - ], - [ - 6, - 6, - 7, - 7 - ], - [ - 6, - 7, - 0, - 5 - ], - [ - 6, - 7, - 0, - 7 - ], - [ - 6, - 7, - 1, - 5 - ], - [ - 6, - 7, - 1, - 7 - ], - [ - 6, - 7, - 4, - 5 - ], - [ - 6, - 7, - 4, - 7 - ], - [ - 6, - 7, - 6, - 5 - ], - [ - 6, - 7, - 6, - 7 - ] -] \ No newline at end of file diff --git a/polymerist/tests/data/stereo_inversion/trimer.sdf b/polymerist/tests/data/stereo_inversion/trimer.sdf new file mode 100644 index 00000000..46a0dce4 --- /dev/null +++ b/polymerist/tests/data/stereo_inversion/trimer.sdf @@ -0,0 +1,123 @@ + + RDKit 3D + + 57 59 0 0 0 0 0 0 0 0999 V2000 + 3.6070 0.5960 -0.6820 C 0 0 0 0 0 0 0 0 0 0 0 0 + 2.7000 -0.0700 0.4010 C 0 0 2 0 0 0 0 0 0 0 0 0 + 1.3780 -0.0620 -0.0250 O 0 0 0 0 0 0 0 0 0 0 0 0 + 0.3820 -0.7580 0.5930 C 0 0 0 0 0 0 0 0 0 0 0 0 + 0.6550 -1.5860 1.4540 O 0 0 0 0 0 0 0 0 0 0 0 0 + -1.0360 -0.4900 0.2610 C 0 0 0 0 0 0 0 0 0 0 0 0 + -2.0660 -1.1680 0.9430 C 0 0 0 0 0 0 0 0 0 0 0 0 + -1.8370 -1.8980 1.7080 H 0 0 0 0 0 0 0 0 0 0 0 0 + -3.4050 -0.9010 0.6470 C 0 0 0 0 0 0 0 0 0 0 0 0 + -4.1890 -1.4240 1.1800 H 0 0 0 0 0 0 0 0 0 0 0 0 + -3.7320 0.0400 -0.3310 C 0 0 0 0 0 0 0 0 0 0 0 0 + -4.7700 0.2460 -0.5560 H 0 0 0 0 0 0 0 0 0 0 0 0 + -2.7190 0.7170 -1.0150 C 0 0 0 0 0 0 0 0 0 0 0 0 + -2.9750 1.4470 -1.7720 H 0 0 0 0 0 0 0 0 0 0 0 0 + -1.3760 0.4560 -0.7220 C 0 0 0 0 0 0 0 0 0 0 0 0 + -0.6070 0.9950 -1.2610 H 0 0 0 0 0 0 0 0 0 0 0 0 + 3.0430 -1.1290 0.4330 H 0 0 0 0 0 0 0 0 0 0 0 0 + 4.6870 0.4120 -0.4900 H 0 0 0 0 0 0 0 0 0 0 0 0 + 3.3580 0.0590 -1.6240 H 0 0 0 0 0 0 0 0 0 0 0 0 + 3.3730 2.0950 -0.9480 C 0 0 0 0 0 0 0 0 0 0 0 0 + 4.2320 2.9970 -0.0470 C 0 0 0 0 0 0 0 0 0 0 0 0 + 3.6650 4.2520 0.0160 O 0 0 0 0 0 0 0 0 0 0 0 0 + 4.2000 5.4240 -0.4050 C 0 0 0 0 0 0 0 0 0 0 0 0 + 5.2220 5.4400 -1.0770 O 0 0 0 0 0 0 0 0 0 0 0 0 + 3.5170 6.6860 -0.0310 C 0 0 0 0 0 0 0 0 0 0 0 0 + 3.9760 7.9330 -0.4940 C 0 0 0 0 0 0 0 0 0 0 0 0 + 4.8400 7.9970 -1.1450 H 0 0 0 0 0 0 0 0 0 0 0 0 + 3.3190 9.1110 -0.1210 C 0 0 0 0 0 0 0 0 0 0 0 0 + 3.6750 10.0670 -0.4820 H 0 0 0 0 0 0 0 0 0 0 0 0 + 2.2030 9.0570 0.7200 C 0 0 0 0 0 0 0 0 0 0 0 0 + 1.6980 9.9690 1.0100 H 0 0 0 0 0 0 0 0 0 0 0 0 + 1.7410 7.8270 1.1870 C 0 0 0 0 0 0 0 0 0 0 0 0 + 0.8780 7.7840 1.8410 H 0 0 0 0 0 0 0 0 0 0 0 0 + 2.3900 6.6470 0.8140 C 0 0 0 0 0 0 0 0 0 0 0 0 + 2.0120 5.7060 1.1940 H 0 0 0 0 0 0 0 0 0 0 0 0 + 4.2900 2.6210 0.9870 H 0 0 0 0 0 0 0 0 0 0 0 0 + 5.2810 3.0090 -0.4200 H 0 0 0 0 0 0 0 0 0 0 0 0 + 2.2850 2.3010 -0.8710 H 0 0 0 0 0 0 0 0 0 0 0 0 + 3.6670 2.3510 -1.9900 H 0 0 0 0 0 0 0 0 0 0 0 0 + 2.7500 0.4830 1.8240 C 0 0 0 0 0 0 0 0 0 0 0 0 + 2.2680 1.7180 2.0890 C 0 0 0 0 0 0 0 0 0 0 0 0 + 2.2620 2.1700 3.0670 H 0 0 0 0 0 0 0 0 0 0 0 0 + 1.8550 2.3370 1.3060 H 0 0 0 0 0 0 0 0 0 0 0 0 + 3.2080 -0.3650 2.7970 O 0 0 0 0 0 0 0 0 0 0 0 0 + 2.9960 -0.3300 4.1470 C 0 0 0 0 0 0 0 0 0 0 0 0 + 2.3530 0.5700 4.6660 O 0 0 0 0 0 0 0 0 0 0 0 0 + 3.4530 -1.4580 4.9940 C 0 0 0 0 0 0 0 0 0 0 0 0 + 3.1710 -1.4660 6.3750 C 0 0 0 0 0 0 0 0 0 0 0 0 + 2.6320 -0.6490 6.8370 H 0 0 0 0 0 0 0 0 0 0 0 0 + 3.5780 -2.5380 7.1740 C 0 0 0 0 0 0 0 0 0 0 0 0 + 3.3520 -2.5360 8.2330 H 0 0 0 0 0 0 0 0 0 0 0 0 + 4.2680 -3.6100 6.6080 C 0 0 0 0 0 0 0 0 0 0 0 0 + 4.5780 -4.4400 7.2290 H 0 0 0 0 0 0 0 0 0 0 0 0 + 4.5540 -3.6130 5.2400 C 0 0 0 0 0 0 0 0 0 0 0 0 + 5.0870 -4.4470 4.8040 H 0 0 0 0 0 0 0 0 0 0 0 0 + 4.1490 -2.5430 4.4340 C 0 0 0 0 0 0 0 0 0 0 0 0 + 4.3760 -2.5700 3.3760 H 0 0 0 0 0 0 0 0 0 0 0 0 + 1 2 1 0 + 1 18 1 0 + 1 19 1 0 + 1 20 1 0 + 2 3 1 0 + 2 17 1 1 + 2 40 1 0 + 3 4 1 0 + 4 5 2 0 + 4 6 1 0 + 6 7 2 0 + 6 15 1 0 + 7 8 1 0 + 7 9 1 0 + 9 10 1 0 + 9 11 2 0 + 11 12 1 0 + 11 13 1 0 + 13 14 1 0 + 13 15 2 0 + 15 16 1 0 + 20 21 1 0 + 20 38 1 0 + 20 39 1 0 + 21 22 1 0 + 21 36 1 0 + 21 37 1 0 + 22 23 1 0 + 23 24 2 0 + 23 25 1 0 + 25 26 2 0 + 25 34 1 0 + 26 27 1 0 + 26 28 1 0 + 28 29 1 0 + 28 30 2 0 + 30 31 1 0 + 30 32 1 0 + 32 33 1 0 + 32 34 2 0 + 34 35 1 0 + 40 41 2 0 + 40 44 1 0 + 41 42 1 0 + 41 43 1 0 + 44 45 1 0 + 45 46 2 0 + 45 47 1 0 + 47 48 2 0 + 47 56 1 0 + 48 49 1 0 + 48 50 1 0 + 50 51 1 0 + 50 52 2 0 + 52 53 1 0 + 52 54 1 0 + 54 55 1 0 + 54 56 2 0 + 56 57 1 0 +M END + +$$$$ diff --git a/polymerist/tests/genutils/fileutils/jsonio/test_serialize.py b/polymerist/tests/genutils/fileutils/jsonio/test_serialize.py new file mode 100644 index 00000000..e27c6aea --- /dev/null +++ b/polymerist/tests/genutils/fileutils/jsonio/test_serialize.py @@ -0,0 +1,295 @@ +'''Unit tests for auto-JSONification of dataclasses, as enabled by TypeSerializers''' + +__author__ = 'Timotej Bernat' +__email__ = 'timotej.bernat@colorado.edu' + +import pytest + +from typing import Any, Type, Union +from dataclasses import dataclass, asdict + +import json +from pathlib import Path +from tempfile import NamedTemporaryFile + +import numpy as np +from numpy.testing import assert_equal +from openmm.unit import Quantity, nanometer, picosecond + +from polymerist.genutils.fileutils.jsonio.jsonify import make_jsonifiable +from polymerist.genutils.fileutils.jsonio.serialize import ( + TypeSerializer, + MultiTypeSerializer, + SetSerializer, + PathSerializer, + QuantitySerializer, + NDArraySerializer, +) +PanTypeSerializer = MultiTypeSerializer( + SetSerializer, + PathSerializer, + QuantitySerializer, + NDArraySerializer, +) + +# Testing JSON encode/decode for particular types +@pytest.mark.parametrize( + 'py_obj, ts_type', + [ + ({'Russell', 'Whitehead', 'anything but this set'}, SetSerializer), + (Path('temp/documents/info.txt'), PathSerializer), + (np.arange(5), NDArraySerializer), + (Quantity(1, nanometer), QuantitySerializer), + ] +) +def test_type_encode( + py_obj : Any, + ts_type : Type[TypeSerializer], +) -> None: + '''Test that a given type serializer can JSON-encode an input of a given type''' + try: + json_str : str = json.dumps(ts_type.encode(py_obj)) + except TypeError: + pytest.fail(f'Could not encode term {py_obj!r} of type {type(py_obj)} to JSON') + +@pytest.mark.parametrize( + 'json_obj, ts_type', + [ + ("{'Russell', 'Whitehead', 'anything but this set'}", SetSerializer), + ('temp/documents/info.txt', PathSerializer), + ({'array': [0, 1, 2, 3, 4], 'dtype': 'int64'}, NDArraySerializer), + pytest.param( + [0, 1, 2, 3, 4], NDArraySerializer, + marks=pytest.mark.xfail( + raises=TypeError, + reason='Array decode requires dict encoding array and dtype', + strict=True, + ) + ), + ({'value': 1, 'unit': 'nanometer'}, QuantitySerializer), + pytest.param( + '1 nanometer', QuantitySerializer, + marks=pytest.mark.xfail( + raises=TypeError, + reason='Quantity decode requires dict encoding magnitude and unit', + strict=True, + ) + ), + ] +) +def test_type_decode( + json_obj : Any, + ts_type : Type[TypeSerializer], +) -> None: + '''Test that a given type serializer can JSON-decode an input of a given type''' + decoded_obj = ts_type.decode(json_obj) + assert issubclass(type(decoded_obj), ts_type.python_type) + + +# Testing fidelity of dataclass JOSNification +## Set +@make_jsonifiable(type_serializer=SetSerializer) +@dataclass +class DummySetContainer: + single_field : int + list_field : list[int] + dict_field : dict[str, Union[str, set]] + +@make_jsonifiable +@dataclass +class DummySetContainerIncomplete: + single_field : int + list_field : list[int] + dict_field : dict[str, Union[str, set]] + +## Path +@make_jsonifiable(type_serializer=PathSerializer) +@dataclass +class DummyPathContainer: + single_field : int + list_field : list[int] + dict_field : dict[str, Union[str, Path]] + +@make_jsonifiable +@dataclass +class DummyPathContainerIncomplete: + single_field : int + list_field : list[int] + dict_field : dict[str, Union[str, Path]] + +## Numpy array +@make_jsonifiable(type_serializer=NDArraySerializer) +@dataclass +class DummyNDArrayContainer: + single_field : np.ndarray + list_field : list[np.ndarray] + dict_field : dict[str, Union[str, np.ndarray]] + +@make_jsonifiable +@dataclass +class DummyNDArrayContainerIncomplete: + single_field : np.ndarray + list_field : list[np.ndarray] + dict_field : dict[str, Union[str, np.ndarray]] + +## Quantity +@make_jsonifiable(type_serializer=QuantitySerializer) +@dataclass +class DummyQuantityContainer: + single_field : Quantity + list_field : list[Quantity] + dict_field : dict[str, Union[str, Quantity]] + +@make_jsonifiable +@dataclass +class DummyQuantityContainerIncomplete: + single_field : Quantity + list_field : list[Quantity] + dict_field : dict[str, Union[str, Quantity]] + +## Multiple types at once +@make_jsonifiable(type_serializer=PanTypeSerializer) +@dataclass +class DummyMultiTypeContainer: + name : str + aliases : set[str] + idx : int + file : Path + times : np.ndarray + length : Quantity + +@make_jsonifiable +@dataclass +class DummyMultiTypeContainerIncomplete: + name : str + aliases : set[str] + idx : int + file : Path + times : np.ndarray + length : Quantity + +## Tests proper +def jsonifiable_dataclass_test_inputs() -> tuple[tuple[Type, dict[str, Any]]]: + '''Compactly produce example test inputs for JSONifiable dataclasses''' + DUMMY_ARGS : dict[type, dict[str, Any]] = { + set : { + 'single_field' : {1,2,3}, + 'list_field' : [{1,2,3}, set('abc')], + 'dict_field' : { + 'numbs' : {4,5,6}, + 'letts' : set('def'), + }, + }, + Path : { + 'single_field' : Path('docs/foo.txt'), + 'list_field' : [Path('docs/bar.txt'), Path('docs/bar.txt')], + 'dict_field' : { + 'foo' : Path('docs/foo.txt'), + 'bar' : Path('docs/bar.txt'), + }, + }, + np.ndarray : { + 'single_field' : np.linspace(0, 1, 4), + 'list_field' : [np.array([0,1,2]), np.arange(5)], + 'dict_field' : { + 'arange' : np.arange(5), + 'linspace' : np.linspace(0, 1, 4), + }, + }, + Quantity : { + 'single_field' : 420.69*nanometer, + 'list_field' : [3.14*picosecond, 0.5772*nanometer], + 'dict_field' : { + 'length' : 1*nanometer, + 'duration' : 5*picosecond, + }, + }, + Any : { + 'name': 'Dracula', + 'aliases': {'D', 'Drac', 'Dracul'}, + 'idx': 42, + 'file': Path('temp/documents/info.txt'), + 'times': np.array([0, 1, 2, 3, 4]), + 'length': Quantity(1*picosecond), + } + } + + return ( + (DummySetContainer, DUMMY_ARGS[set]), + pytest.param( + DummySetContainerIncomplete, DUMMY_ARGS[set], + marks=pytest.mark.xfail( + raises=TypeError, + reason='Forgot to include TypeSerializer in dataclass definition', + strict=True, + ) + ), + (DummyPathContainer, DUMMY_ARGS[Path]), + pytest.param( + DummyPathContainerIncomplete, DUMMY_ARGS[Path], + marks=pytest.mark.xfail( + raises=TypeError, + reason='Forgot to include TypeSerializer in dataclass definition', + strict=True, + ) + ), + (DummyNDArrayContainer, DUMMY_ARGS[np.ndarray]), + pytest.param( + DummyNDArrayContainerIncomplete, DUMMY_ARGS[np.ndarray], + marks=pytest.mark.xfail( + raises=TypeError, + reason='Forgot to include TypeSerializer in dataclass definition', + strict=True, + ) + ), + (DummyQuantityContainer, DUMMY_ARGS[Quantity]), + pytest.param( + DummyQuantityContainerIncomplete, DUMMY_ARGS[Quantity], + marks=pytest.mark.xfail( + raises=TypeError, + reason='Forgot to include TypeSerializer in dataclass definition', + strict=True, + ) + ), + (DummyMultiTypeContainer, DUMMY_ARGS[Any]), + pytest.param( + DummyMultiTypeContainerIncomplete, DUMMY_ARGS[Any], + marks=pytest.mark.xfail( + raises=TypeError, + reason='Forgot to include TypeSerializer in dataclass definition', + strict=True, + ) + ), + ) + +@pytest.mark.parametrize( + 'container_type, args', + jsonifiable_dataclass_test_inputs() +) +def test_jsonify_dataclass_serialize( + container_type : Type, + args : dict[str, Any], +) -> None: + ''' + Test that custom TypeSerializers passed to dataclasses + wrapped with `make_jsonifiable` are able to actually + serialize the target types to JSON + ''' + test_obj = container_type(**args) + with NamedTemporaryFile('r', suffix='.json') as file: + test_obj.to_file(file.name) # DEV: no assert - just checking that a TypeError is not raised here + +@pytest.mark.parametrize( + 'container_type, args', + jsonifiable_dataclass_test_inputs() +) +def test_jsonify_dataclass_deserialize( + container_type : Type, + args : dict[str, Any], +) -> None: + test_obj = container_type(**args) + with NamedTemporaryFile('r', suffix='.json') as file: + test_obj.to_file(file.name) + test_obj_loaded = container_type.from_file(file.name) + + assert_equal(asdict(test_obj), asdict(test_obj_loaded)) \ No newline at end of file diff --git a/polymerist/tests/polymers/building/test_linear.py b/polymerist/tests/polymers/building/test_linear.py index 2c086a97..6773c22b 100644 --- a/polymerist/tests/polymers/building/test_linear.py +++ b/polymerist/tests/polymers/building/test_linear.py @@ -4,16 +4,26 @@ __email__ = 'timotej.bernat@colorado.edu' import pytest +from typing import Optional from collections import Counter +from itertools import product as cartesian_product -from polymerist.polymers.monomers.repr import MonomerGroup -from polymerist.polymers.monomers.fragments import PE_FRAGMENTS, MPD_TMC_FRAGMENTS, PEG_PLGA_FRAGMENTS +from polymerist.rdutils.sanitization import explicit_mol_from_SMILES -from polymerist.polymers.building import build_linear_polymer +from polymerist.polymers.monomers.repr import MonomerGroup +from polymerist.polymers.monomers.fragments import ( + PE_FRAGMENTS, + MPD_TMC_FRAGMENTS, + PEG_PLGA_FRAGMENTS, + HALOGENATED_HYDROCARBON_FRAGMENTS, +) +from polymerist.polymers.building.linear import build_linear_polymer +from polymerist.polymers.building.mbconvert import mbmol_to_rdmol from polymerist.polymers.exceptions import MorphologyError, PartialBlockSequence, EmptyBlockSequence +# FIXTURES AND INPUT SETUP FOR TESTS @pytest.fixture(scope='function') def monogrp_polyethylene() -> MonomerGroup: return MonomerGroup(monomers=PE_FRAGMENTS) @@ -26,7 +36,121 @@ def monogrp_mpd_tmc() -> MonomerGroup: def monogrp_peg_plga() -> MonomerGroup: return MonomerGroup(monomers=PEG_PLGA_FRAGMENTS) +@pytest.fixture(scope='function') +def monogrp_halogen_marked() -> MonomerGroup: + return MonomerGroup(monomers=HALOGENATED_HYDROCARBON_FRAGMENTS) + +SEQS_TO_HALOGEN_KERNEL : dict[tuple[str, ...], tuple[str, Optional[dict[str, str]]]] = { + # defaulted + ('A', 'S', '$') : ('C(F)', None) , # for homopolymer, ANY single symbol should given same behavior + ('AA', 'BB', '$$') : ('C(F)C(F)', None) , # BB is confusing, but point is this is still a homopolymer, so symbol choice still doesn't matter + ('BAB', 'qaq', '101', '212') : ('C(F)(F)C(F)C(F)(F)', None), + ('BADC', 'badc', '2143') : ('C(F)(F)C(F)C(Cl)(Cl)C(Cl)', None), + ('BACC', 'bacc', '1022') : ('C(F)(F)C(F)C(Cl)C(Cl)', None), + ('ABCFED', 'acezyx', '012543', '013964') : ('C(F)C(F)(F)C(Cl)C(Br)(Br)C(Br)C(Cl)(Cl)', None), + # custom mapping + ('BADC',) : ('C(Br)(Br)C(Cl)C(F)(F)C(Br)', { + 'B' : 'brom_mid_2', + 'A' : 'chlor_mid_1', + 'D' : 'fluor_mid_2', + 'C' : 'brom_mid_1', + }), + ('ABA',) : ('C(Cl)C(Br)(Br)C(Cl)', { + 'A' : 'chlor_mid_1', + 'B' : 'brom_mid_2', + }), + ('qyz',) : ('C(F)C(Br)(Br)C(F)', { + 'q' : 'fluor_mid_1', + 'y' : 'brom_mid_2', + 'z' : 'fluor_mid_1', # duplication of mapped units IS allowed + }) + +} + +TERM_ORIENT_TO_SMILES_HALOGEN : dict[str, dict[str, str]] = { + # fluorines + 'fluor_term_1' : { + 'head' : 'OC(F)', + 'tail' : 'C(F)O', + }, + 'fluor_term_2' : { + 'head' : 'OC(=O)C(F)', + 'tail' : 'C(F)C(=O)O', + }, + # chlorines + 'chlor_term_1' : { + 'head' : 'OC(Cl)', + 'tail' : 'C(Cl)O', + }, + 'chlor_term_2' : { + 'head' : 'OC(=O)C(Cl)', + 'tail' : 'C(Cl)C(=O)O', + }, + # bromines + 'brom_term_1' : { + 'head' : 'OC(Br)', + 'tail' : 'C(Br)O', + }, + 'brom_term_2' : { + 'head' : 'OC(=O)C(Br)', + 'tail' : 'C(Br)C(=O)O', + }, +} + +def compile_sequence_test_inputs(kernel_repeats : tuple[int, ...]) -> list[tuple[str, dict[str, str], int, str]]: + ''' + Helper methods for compiling flattened inputs for linear + polymer builder sequence testing with default sequence map + + (i.e. symbol matched to repeat unit based on order alone) + ''' + # pick out term group orientation dicts and accompaying SMILES caps + term_orients_and_smiles : list[tuple[dict[str, str], tuple[str, str]]] = [ + ( # special case for unspecified term orients - inferred to be first two terminal groups + dict(), + ( + TERM_ORIENT_TO_SMILES_HALOGEN['fluor_term_1']['head'], + TERM_ORIENT_TO_SMILES_HALOGEN['fluor_term_2']['tail'], + ), + ) + ] + + TERM_ORIENT_PAIRS : tuple[tuple[str, str], ...] = ( + ('fluor_term_1', 'fluor_term_2'), + ('chlor_term_1', 'chlor_term_2'), + # ('chlor_term_2', 'chlor_term_1'), + ('fluor_term_2', 'brom_term_1'), + ('chlor_term_1', 'brom_term_1'), + ) + # for (runame_head, runame_tail) in combinations(TERM_ORIENT_TO_SMILES_HALOGEN, 2): # opting to remove ALL combinations to avoid ballooning number of tests + for (runame_head, runame_tail) in TERM_ORIENT_PAIRS: # opting to remove ALL combinations to avoid ballooning number of tests + term_orients : dict[str, str] = { + 'head' : runame_head, + 'tail' : runame_tail, + } + term_smiles : tuple[str, str] = ( + TERM_ORIENT_TO_SMILES_HALOGEN[runame_head]['head'], + TERM_ORIENT_TO_SMILES_HALOGEN[runame_tail]['tail'], + ) + term_orients_and_smiles.append( (term_orients, term_smiles) ) + + # pick out individual test inputs from cartesian product over options + sequence_test_inputs : list[tuple[str, dict[str, str], int, str]] = [] + for ((sequences, (kernel, mapping)), (term_orient, term_smiles), n_kernel_repeats) in cartesian_product( + SEQS_TO_HALOGEN_KERNEL.items(), + term_orients_and_smiles, + kernel_repeats, + ): + head_smiles, tail_smiles = term_smiles + smiles_expected = ''.join([head_smiles] + n_kernel_repeats*[kernel] + [tail_smiles]) + + for sequence in sequences: + n_monomers = 2 + len(sequence)*n_kernel_repeats # 2 accounts for end groups + sequence_test_inputs.append( (sequence, term_orient, n_monomers, smiles_expected, mapping) ) + + return sequence_test_inputs +# TESTS PROPER @pytest.mark.parametrize( 'monomers, term_orient, n_monomers, sequence, minimize_sequence, allow_partial_sequences, energy_minimize', [ @@ -84,17 +208,18 @@ def monogrp_peg_plga() -> MonomerGroup: ('monogrp_peg_plga', {}, 40, 'ABCB', True, True, True), # test longer energy min ] ) -def test_build_linear_polymer( - monomers : MonomerGroup, - term_orient : dict[str, str], - n_monomers : int, - sequence : str, - minimize_sequence : bool, - allow_partial_sequences : bool, - energy_minimize : bool, - request : pytest.FixtureRequest, # allows for fixture expansion in parameterized arguments - ) -> None: - '''Test linear polymer builder behavior under varing sets of parameters''' +def test_build_linear_polymer_contributions( + monomers : MonomerGroup, + term_orient : dict[str, str], + n_monomers : int, + sequence : str, + minimize_sequence : bool, + allow_partial_sequences : bool, + energy_minimize : bool, + request : pytest.FixtureRequest, # allows for fixture expansion in parameterized arguments +) -> None: + '''Test linear polymer builder assembles chains with right number of + repeat units AND right number of atoms per repeat unit''' monomers = request.getfixturevalue(monomers) # unpack fixtures into their respective values monomers.term_orient = term_orient # this edit makes it VITAL that fixtures be function-level @@ -110,11 +235,11 @@ def test_build_linear_polymer( # characterize middle monomers n_rep_units = len(polymer.children) - residue_sizes : dict[str, int] = {} - residue_counts = Counter() # TODO: make use of this for checks!! + repeat_unit_sizes : dict[str, int] = {} + repeat_unit_counts = Counter() # TODO: make use of this for checks!! for middle_monomers in polymer.children: - residue_sizes[middle_monomers.name] = middle_monomers.n_particles - residue_counts[middle_monomers.name] += 1 + repeat_unit_sizes[middle_monomers.name] = middle_monomers.n_particles + repeat_unit_counts[middle_monomers.name] += 1 # characterize end groups end_groups_requested = set(resname for head_or_tail, (resname, mol) in monomers.linear_end_groups().items()) @@ -122,14 +247,43 @@ def test_build_linear_polymer( for end_group in polymer.end_groups: if end_group is not None: end_groups_used.add(end_group.name) - residue_sizes[end_group.name] = end_group.n_particles - residue_counts[middle_monomers.name] += 1 + repeat_unit_sizes[end_group.name] = end_group.n_particles + repeat_unit_counts[middle_monomers.name] += 1 total_reps_match = (n_rep_units == n_monomers) contribs_match = all(num_monomers == monomers.contributions()[resname][0] - for resname, num_monomers in residue_sizes.items() + for resname, num_monomers in repeat_unit_sizes.items() ) end_groups_correct = (end_groups_used == end_groups_requested) # counts_match = ... - assert all([total_reps_match, contribs_match, end_groups_correct]) #, and counts_match ) \ No newline at end of file + assert all([total_reps_match, contribs_match, end_groups_correct]) #, and counts_match ) + +@pytest.mark.parametrize( + 'sequence, term_orient, n_monomers, smiles_expected, sequence_map', + compile_sequence_test_inputs(kernel_repeats=(2,)) +) +def test_build_linear_polymer_sequence( + monogrp_halogen_marked : MonomerGroup, + sequence : str, + term_orient : dict[str, str], + n_monomers : int, + smiles_expected : str, + sequence_map : Optional[dict[str, str]], +) -> None: + '''Test that repeat units are assembled in the expected order for a given input to the linear polymer builder''' + mol_expected = explicit_mol_from_SMILES(smiles_expected) + + monogrp_halogen_marked.term_orient = term_orient + chain = build_linear_polymer( + monogrp_halogen_marked, + n_monomers=n_monomers, + sequence=sequence, + allow_partial_sequences=False, + add_Hs=False, + energy_minimize=False, # No point minimzing, since we only care about order of groups, not coordinates + sequence_map=sequence_map, + ) + mol_actual = mbmol_to_rdmol(chain) + + assert mol_expected.HasSubstructMatch(mol_actual) and mol_actual.HasSubstructMatch(mol_expected) diff --git a/polymerist/tests/polymers/building/test_sequencing.py b/polymerist/tests/polymers/building/test_sequencing.py index 272fd4d1..e2036bd7 100644 --- a/polymerist/tests/polymers/building/test_sequencing.py +++ b/polymerist/tests/polymers/building/test_sequencing.py @@ -7,16 +7,24 @@ from dataclasses import asdict import pytest -from pathlib import Path -from polymerist.polymers.building.sequencing import LinearCopolymerSequencer as LCS -from polymerist.polymers.exceptions import EmptyBlockSequence, PartialBlockSequence, InsufficientChainLength, EndGroupDominatedChain +from polymerist.polymers.building.sequencing import LinearCopolymerSequencer +from polymerist.polymers.exceptions import ( + EmptyBlockSequence, + PartialBlockSequence, + InsufficientChainLength, + EndGroupDominatedChain, +) @pytest.fixture -def sequencer() -> LCS: +def sequencer() -> LinearCopolymerSequencer: '''A sample sequencer with known, valid inputs''' - return LCS(sequence_kernel='ABAB', n_repeat_units=14, n_repeat_units_terminal=2) + return LinearCopolymerSequencer( + sequence_kernel='ABAB', + n_repeat_units=14, + n_repeat_units_terminal=2, + ) @pytest.mark.parametrize( 'inputs', @@ -54,9 +62,9 @@ def sequencer() -> LCS: ) def test_LCS_input_validation(inputs : dict[str, Any]) -> None: '''Test that invalid Sequence input are correctly rejected''' - _ = LCS(**inputs) # no assert needed, just checking when initialization completes + _ = LinearCopolymerSequencer(**inputs) # no assert needed, just checking when initialization completes -def test_LCS_copying(sequencer : LCS) -> None: +def test_LCS_copying(sequencer : LinearCopolymerSequencer) -> None: '''Test that sequencers are properly copied in a read-only manner''' sequencer_clone = sequencer.copy() @@ -72,11 +80,11 @@ def test_LCS_copying(sequencer : LCS) -> None: @pytest.mark.parametrize( 'sequencer, expected_kernel', [ - (LCS('ABC', n_repeat_units=12), 'ABC') , # test irrreducible case - (LCS('ABAB', n_repeat_units=12), 'AB'), # test unreduced case + (LinearCopolymerSequencer('ABC', n_repeat_units=12), 'ABC') , # test irrreducible case + (LinearCopolymerSequencer('ABAB', n_repeat_units=12), 'AB'), # test unreduced case ] ) -def test_LCS_reduction(sequencer : LCS, expected_kernel : str) -> None: +def test_LCS_reduction(sequencer : LinearCopolymerSequencer, expected_kernel : str) -> None: '''Test that shortest repeating subsequences of sequencer kernels are correctly identified''' sequencer.reduce() assert sequencer.sequence_kernel == expected_kernel @@ -85,10 +93,10 @@ def test_LCS_reduction(sequencer : LCS, expected_kernel : str) -> None: 'sequencer, allow_partials, expected_sequence, expected_length', [ # tests for homopolymers - (LCS('A', 5, 1), True , 'A', 4), - (LCS('A', 5, 1), False, 'A', 4), # partial block single-monomer sequence will never exist, so "allow_partial_sequences" setting shouldn't matter) + (LinearCopolymerSequencer('A', 5, 1), True , 'A', 4), + (LinearCopolymerSequencer('A', 5, 1), False, 'A', 4), # partial block single-monomer sequence will never exist, so "allow_partial_sequences" setting shouldn't matter) pytest.param( - LCS('A', 1, 1), True, 'A', 1, # test that all-end group (i.e. no middle monomer) case is correctly rejected + LinearCopolymerSequencer('A', 1, 1), True, 'A', 1, # test that all-end group (i.e. no middle monomer) case is correctly rejected marks=pytest.mark.xfail( raises=InsufficientChainLength, reason='No middle monomers can be accomodated', @@ -96,28 +104,28 @@ def test_LCS_reduction(sequencer : LCS, expected_kernel : str) -> None: ), ), # tests for "true" copolymers - (LCS('ABC', 10, 2), True, 'ABCABCAB', 1), + (LinearCopolymerSequencer('ABC', 10, 2), True, 'ABCABCAB', 1), pytest.param( - LCS('ABC', 10, 2), False, 'ABCABCAB', 1, # test that partial-sequence ban correctly blocks partial sequences... + LinearCopolymerSequencer('ABC', 10, 2), False, 'ABCABCAB', 1, # test that partial-sequence ban correctly blocks partial sequences... marks=pytest.mark.xfail( raises=PartialBlockSequence, reason='Partial sequence repeats have not been allowed', strict=True, ), ), - (LCS('ABC', 11, 2), False, 'ABC', 3), # ...unless the resulting sequence happens to be a whole multiple + (LinearCopolymerSequencer('ABC', 11, 2), False, 'ABC', 3), # ...unless the resulting sequence happens to be a whole multiple pytest.param( - LCS('ABC', 2, 2), True, '', 1, # test that all-end group (i.e. no middle monomer) case is correctly rejected... + LinearCopolymerSequencer('ABC', 2, 2), True, '', 1, # test that all-end group (i.e. no middle monomer) case is correctly rejected... marks=pytest.mark.xfail( raises=InsufficientChainLength, reason='No middle monomers can be accomodated', strict=True, ), ), - (LCS('ABC', 4, 2), True, 'AB', 1), # ... and finally, check that nonempty sequences SMALLER than the kernel are also recognized if partials are permitted + (LinearCopolymerSequencer('ABC', 4, 2), True, 'AB', 1), # ... and finally, check that nonempty sequences SMALLER than the kernel are also recognized if partials are permitted ] ) -def test_LCS_procrustean_alignment(sequencer : LCS, allow_partials : bool, expected_sequence : str, expected_length : int) -> None: +def test_LCS_procrustean_alignment(sequencer : LinearCopolymerSequencer, allow_partials : bool, expected_sequence : str, expected_length : int) -> None: '''Test capability (and prechecks) for fitting sequence to target chain length''' seq, n_reps = sequencer.procrustean_alignment(allow_partial_sequences=allow_partials) assert (seq == expected_sequence) and (n_reps == expected_length) \ No newline at end of file diff --git a/polymerist/tests/rdutils/rdcoords/piercing.py b/polymerist/tests/rdutils/rdcoords/test_piercing.py similarity index 95% rename from polymerist/tests/rdutils/rdcoords/piercing.py rename to polymerist/tests/rdutils/rdcoords/test_piercing.py index c25806e9..9b5d92a2 100644 --- a/polymerist/tests/rdutils/rdcoords/piercing.py +++ b/polymerist/tests/rdutils/rdcoords/test_piercing.py @@ -1,4 +1,7 @@ -'''Unit tests for `sanitization` package''' +'''Unit tests for `piercing` package''' + +__author__ = 'Timotej Bernat' +__email__ = 'timotej.bernat@colorado.edu' import pytest from pathlib import Path diff --git a/polymerist/tests/rdutils/rdcoords/test_tiling.py b/polymerist/tests/rdutils/rdcoords/test_tiling.py new file mode 100644 index 00000000..9e8a0b30 --- /dev/null +++ b/polymerist/tests/rdutils/rdcoords/test_tiling.py @@ -0,0 +1,47 @@ +'''Unit tests for `tiling` package''' + +__author__ = 'Timotej Bernat' +__email__ = 'timotej.bernat@colorado.edu' + +import pytest +from pathlib import Path + +import numpy as np + +from rdkit.Chem import Mol, SDMolSupplier, AssignStereochemistryFrom3D +from rdkit.Chem.rdMolTransforms import ComputeCanonicalTransform, TransformConformer + +from polymerist.genutils.importutils.pkginspect import get_dir_path_within_package +from polymerist.tests import data as testdata +from polymerist.rdutils.rdcoords.tiling import tile_lattice_with_rdmol + + +@pytest.fixture +def test_structures_dir_path() -> Path: + return get_dir_path_within_package('stereo_inversion', testdata) + +@pytest.fixture +def chiral_mol(test_structures_dir_path : Path) -> Mol: + # DEV: would've liked to have a SMILES-based MRE, but is difficult to know in advance when inversion will happen; + # using example "from the wild" which has been observed to reliably exhibit this bad behavior + with SDMolSupplier(str(test_structures_dir_path / 'trimer.sdf'), sanitize=True) as suppl: + return suppl[0] + +def test_canon_transform_stereo_inversion(chiral_mol : Mol) -> None: + ''' + Test that RDKit's principal axis alignment doesn't inadvertently + invert coordinate handedness and therefore stereochemistry, + or that at least this is caught and corrected when it happens + + Made necessary by an RDKit bug, (only patched in RDKit 2025.09.x) reported in + https://github.com/rdkit/rdkit/issues/8992 and even earlier in https://github.com/rdkit/rdkit/issues/8720 + ''' # TODO: invoke tiler to check that internal patches cover this issue (current test only test for underlying issue, which my code can't fix) + lattice = np.zeros((1, 3), dtype=float) + chiral_atom_idx : int = 1 # atom known to be stereocenter in this structure + + chiral_tag_init = chiral_mol.GetAtomWithIdx(chiral_atom_idx).GetChiralTag() + tiled_mol = tile_lattice_with_rdmol(chiral_mol, lattice, rotate_randomly=False, conf_id=0) + AssignStereochemistryFrom3D(tiled_mol) # ensure that stereo is updated post-transform + chiral_tag_final = tiled_mol.GetAtomWithIdx(chiral_atom_idx).GetChiralTag() + + assert (chiral_tag_init == chiral_tag_final) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7cde46cf..cc2f8517 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,8 @@ requires-python = "~=3.11" # Declare any run-time dependencies that should be installed with the package. dependencies = [ "importlib-resources", # added to allow incorporation of `data` later on - "numpy<2.0.0", # DEV: pinning this, as leaving it unpinned causes errors when improting mbuild (deprecated numpy.vecdot function) + "setuptools<82.0.0", # added to avoid CI errors due to pkg_resources deprecation + "numpy<2.0.0", # DEV: pinning this, as leaving it unpinned causes errors when importing mbuild (deprecated numpy.vecdot function) "scipy", "pandas", "rich", @@ -31,11 +32,11 @@ dependencies = [ "anytree", "networkx", "rdkit", + "mdtraj", "openmm", - # DEV: at time of writing, latest stable version of lammps shipped w/ binaries on conda + # DEV: as of 08/18/2025, latest stable version of lammps shipped w/ binaries on conda # lammps-dependent code is unrunnable (citing MPI OSError) for more recent builds (i.e. 2025.MM.DD) "lammps<=2024.08.29", - "mdtraj", ] # Update the urls once the hosting is set up.