diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 57b2ddc..7da7083 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -34,7 +34,7 @@ jobs: - name: Install dependencies run: | - poetry install --with dev --no-interaction + poetry install --with dev --extras "rdkit gui" --no-interaction - name: Test with pytest (coverage) run: | diff --git a/README.md b/README.md index 6e4f552..2e00635 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,197 @@ - xxxxxxxxxxxxxxxxxx - xx xx - xxx QEPest xxx - xx xx - xxxxxxxxxxxxxxxxxx +# pythonQEPest -# Introduction +[![pythonQEPest](https://github.com/PonyLianna/pythonQEPest/actions/workflows/python-package.yml/badge.svg)](https://github.com/PonyLianna/pythonQEPest/actions/workflows/python-package.yml) + + +Python implementation of QEPest (Quantitative Estimation of Pesticide), a program for scoring molecules as herbicides (QEH), insecticides (QEI), and fungicides (QEF). + +Originally published in: [J Cheminform - QEPest](https://jcheminf.biomedcentral.com/articles/10.1186/s13321-014-0042-6) +Original Program Link is [here](https://static-content.springer.com/esm/art%3A10.1186%2Fs13321-014-0042-6/MediaObjects/13321_2014_42_MOESM2_ESM.zip) + +## Features + +- Calculate pesticide scores for herbicidal, insecticidal, and fungicidal activity +- Command-line interface (CLI) +- Graphical User Interface (GUI) +- Support for JSON and TXT output formats + +### Pre-built binaries + +Download ready-to-use executables from [Releases](https://github.com/PonyLianna/pythonQEPest/releases): + +| Platform | Download | +|----------|----------| +| Windows CLI | [main.exe](https://github.com/PonyLianna/pythonQEPest/releases/download/v2.0.0-alpha/main.exe) | +| Windows GUI | [gui.exe](https://github.com/PonyLianna/pythonQEPest/releases/download/v2.0.0-alpha/gui.exe) | -The rewritten version of Java QEPest. Made by PonyLianna (https://github.com/PonyLianna). ## Installation -You can use a .exe version (which you can find at https://github.com/PonyLianna/pythonQEPest/releases) -or run it by yourself. +```bash +pip install pythonQEPest +``` + +Or from source: + +```bash +git clone https://github.com/PonyLianna/pythonQEPest.git +cd pythonQEPest +poetry install +``` + +### With GUI support + +```bash +pip install pythonQEPest[gui] +# or +poetry install --extras ui +``` + + +## Quick Start + +### CLI + +```bash +pythonqepest -i data.txt -o result.txt +pythonqepest --input data.txt --format json +pythonqepest -v # show version +``` + +### GUI + +```bash +pythonqepest-gui +# or +python -m pythonQEPest.gui.gui +``` + +### Python API + +```python +from pythonQEPest import QEPest, QEPestInput + +# Create input data +qepest = QEPest() +qepest.initialize_coefficients() +qepest.initialize_normalisers() + +# From individual values +inp = QEPestInput( + name="mol1", + mol_weight=240.2127, + log_p=3.2392, + hbond_acceptors=5, + hbond_donors=1, + rotatable_bonds=4, + aromatic_rings=1 +) + +result = qepest.compute_params(inp) + +print(result.name) # mol1 +print(result.data.qe_herb) # 0.8511 +print(result.data.qe_insect) # 0.5339 +print(result.data.qe_fung) # 0.6224 + +# Convert to array +print(result.to_array()) # ["mol1", 0.6224, 0.8511, 0.5339] +``` + +## Input Format + +The input file should be a tab-separated text file with the following columns: + +| Column | Description | +|--------|-------------| +| Name | Molecule name | +| MW | Molecular weight (g/mol) | +| LogP | Hydrophobicity (octanol-water partition coefficient) | +| HBA | Number of hydrogen bond acceptors | +| HBD | Number of hydrogen bond donors | +| RB | Number of rotatable bonds | +| arR | Number of aromatic rings | + +Example `data.txt`: -To be able to do it you'll need `Python >3.10 + pip3` (https://www.python.org/downloads/) +``` +Name MW LogP HBA HBD RB arR +mol1 240.2127 3.2392 5 1 4 1 +mol2 249.091 3.0273 3 1 5 1 +mol3 308.354 2.1086 1 0 7 1 +``` -Further installation (I skip venv here): +## Output Format -`pip install poetry` +### TXT (default output for CLI) -`poetry install` +``` +Name QEF QEH QEI +mol1 0.6224 0.8511 0.5339 +mol2 0.7310 0.9750 0.6913 +``` -`python ./pythonQEPest/main.py` +### JSON -And you're ready to work with it! Just fill your data.txt with anything you want to process and execute this script. +```json +[ + {"name": "mol1", "qe_fung": 0.6224, "qe_herb": 0.8511, "qe_insect": 0.5339}, + {"name": "mol2", "qe_fung": 0.731, "qe_herb": 0.975, "qe_insect": 0.6913} +] +``` -If you want a GUI: `python pythonQEPest/gui/gui.py` +## Configuration -## Where to find original of this program? +### Custom Coefficients -https://jcheminf.biomedcentral.com/articles/10.1186/s13321-014-0042-6#MOESM2 +```python +from pythonQEPest import QEPest -# Original README.md +custom_coefficients = { + "herb": [ + (70.77, 283.0, 84.97, -1.185), + (93.81, 3.077, 1.434, 0.6164), + # ... 6 tuples total + ], + "insect": [...], + "fung": [...] +} -QEPest is a free Java program addressing the filed of agrochemicals. It allows the scoring of molecules -as herbicides (QEH), insecticides (QEI) and fungicides (QEF) according to pesticide class-specific scoring functions. +qepest = QEPest(coefficients=custom_coefficients) +``` -Although these are basic molecular descriptors, multiple approximations of logP are available. The parameterization -of the desirability functions has been performed using descriptors generated with JChem (6.0.0, 2013, ChemAxon, -http://www.chemaxon.com). Hence, in order to assure maximum accuracy, we recommend the usage of ChemAxon’s logP. +### Custom Normalisers -Before running QEPest.jar, please make sure: +```python +from pythonQEPest import QEPest +from pythonQEPest.dto.normalisation.Normaliser import Normaliser -- Java Runtime Engine 1.6 or later installed is installed on your computer -- The file "data.txt", containing the molecules to be scored, respects the structure as described below (### Input - file ###) - (tab sepatated file with header, each molecule in a different row) -- QEPest.jar and data.txt are placed in the same directory +qepest = QEPest(normalisers={"herb": Normaliser(...), ...}) +``` -### Input file +## CLI Options -The input for QEPest consists of a tab-separated text file containing molecules (in rows) and seven columns (in this -order): +| Option | Description | Default | +|--------|-------------|---------| +| `-i`, `--input` | Input file path | data.txt | +| `-o`, `--output` | Output file path | data.out.txt | +| `-f`, `--format` | Output format (json, txt) | txt | +| `-v`, `--version` | Show version | - | -- molecule name (Name) -- molecular weight (MW) -- hydrophobicity (LogP) -- number of hydrogen bond acceptors (HBA) -- number of hydrogen bond donors (HBD) -- number of rotatable bounds (RB) -- number of aromatic rings (arR) +## Development -Example of data.txt -Name MW LogP HBA HBD RB arR -mol1 240.2127 3.2392 5 1 4 1 -mol2 249.091 3.0273 3 1 5 1 -mol3 308.354 2.1086 1 0 7 1 -mol4 360.444 4.0137 3 0 8 0 -mol5 295.335 4.9335 2 0 1 1 +### Run tests -#### Running QEPest.jar +```bash +poetry run pytest +``` -QEPest.jar will read the data.txt file and compute QEH, QEI and QEF. If an error occurs in a row (e.g., missing value, -bad number of fields etc), the an error message will indicate the molecule and the computation will proceed to the next -row. An message will indicate the end of the and an output file (i.e., data.txt.out) will be written in the same -directory. +### Build -For any question please write to 5orin.4vram@gmail.com +```bash +# CLI executable +poetry run poe build-simple -Have fun!!! +# GUI executable +poetry run poe build-gui +``` diff --git a/poetry.lock b/poetry.lock index 90ab610..17c8e08 100644 --- a/poetry.lock +++ b/poetry.lock @@ -741,14 +741,14 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "identify" -version = "2.6.16" +version = "2.6.17" description = "File identification library for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0"}, - {file = "identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980"}, + {file = "identify-2.6.17-py2.py3-none-any.whl", hash = "sha256:be5f8412d5ed4b20f2bd41a65f920990bdccaa6a4a18a08f1eefdcd0bdd885f0"}, + {file = "identify-2.6.17.tar.gz", hash = "sha256:f816b0b596b204c9fdf076ded172322f2723cf958d02f9c3587504834c8ff04d"}, ] [package.extras] @@ -1013,6 +1013,89 @@ files = [ {file = "nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb"}, ] +[[package]] +name = "numpy" +version = "2.4.2" +description = "Fundamental package for array computing in Python" +optional = true +python-versions = ">=3.11" +groups = ["main"] +markers = "extra == \"rdkit\"" +files = [ + {file = "numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825"}, + {file = "numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1"}, + {file = "numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7"}, + {file = "numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73"}, + {file = "numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1"}, + {file = "numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32"}, + {file = "numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390"}, + {file = "numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413"}, + {file = "numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda"}, + {file = "numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695"}, + {file = "numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3"}, + {file = "numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a"}, + {file = "numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1"}, + {file = "numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e"}, + {file = "numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27"}, + {file = "numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548"}, + {file = "numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f"}, + {file = "numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460"}, + {file = "numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba"}, + {file = "numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f"}, + {file = "numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85"}, + {file = "numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa"}, + {file = "numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c"}, + {file = "numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979"}, + {file = "numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98"}, + {file = "numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef"}, + {file = "numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7"}, + {file = "numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499"}, + {file = "numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb"}, + {file = "numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7"}, + {file = "numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110"}, + {file = "numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622"}, + {file = "numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71"}, + {file = "numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262"}, + {file = "numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913"}, + {file = "numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab"}, + {file = "numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82"}, + {file = "numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f"}, + {file = "numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554"}, + {file = "numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257"}, + {file = "numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657"}, + {file = "numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b"}, + {file = "numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1"}, + {file = "numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b"}, + {file = "numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000"}, + {file = "numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1"}, + {file = "numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74"}, + {file = "numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a"}, + {file = "numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325"}, + {file = "numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909"}, + {file = "numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a"}, + {file = "numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a"}, + {file = "numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75"}, + {file = "numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05"}, + {file = "numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308"}, + {file = "numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef"}, + {file = "numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d"}, + {file = "numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8"}, + {file = "numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5"}, + {file = "numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e"}, + {file = "numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a"}, + {file = "numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443"}, + {file = "numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236"}, + {file = "numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181"}, + {file = "numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082"}, + {file = "numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a"}, + {file = "numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920"}, + {file = "numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821"}, + {file = "numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb"}, + {file = "numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0"}, + {file = "numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0"}, + {file = "numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae"}, +] + [[package]] name = "packaging" version = "26.0" @@ -1039,14 +1122,14 @@ files = [ [[package]] name = "pbs-installer" -version = "2026.2.11" +version = "2026.3.3" description = "Installer for Python Build Standalone" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "pbs_installer-2026.2.11-py3-none-any.whl", hash = "sha256:0a1eb8bc6c0a53f381b8dc09c18c0d7aa9e6a2495b0bf02b27d48af6b6b4d01f"}, - {file = "pbs_installer-2026.2.11.tar.gz", hash = "sha256:7eb2730aaa8e2a9aa51db3871e494d058dbab64328deec1fc7bdbbc68578167f"}, + {file = "pbs_installer-2026.3.3-py3-none-any.whl", hash = "sha256:4ab076205def78f6a69dd4a7ff9264a016eec292d96f7aa1ea238ccb9dd09e8b"}, + {file = "pbs_installer-2026.3.3.tar.gz", hash = "sha256:b6636d2301709ec1f37640733ba3be3c02bd98558fcba6e4ca157876660f62dd"}, ] [package.dependencies] @@ -1071,6 +1154,116 @@ files = [ {file = "pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632"}, ] +[[package]] +name = "pillow" +version = "12.1.1" +description = "Python Imaging Library (fork)" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"rdkit\"" +files = [ + {file = "pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0"}, + {file = "pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4"}, + {file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e"}, + {file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff"}, + {file = "pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40"}, + {file = "pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23"}, + {file = "pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9"}, + {file = "pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32"}, + {file = "pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b"}, + {file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5"}, + {file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d"}, + {file = "pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c"}, + {file = "pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563"}, + {file = "pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80"}, + {file = "pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052"}, + {file = "pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0"}, + {file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3"}, + {file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35"}, + {file = "pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a"}, + {file = "pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6"}, + {file = "pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523"}, + {file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e"}, + {file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9"}, + {file = "pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6"}, + {file = "pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60"}, + {file = "pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717"}, + {file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a"}, + {file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029"}, + {file = "pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b"}, + {file = "pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1"}, + {file = "pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a"}, + {file = "pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da"}, + {file = "pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13"}, + {file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf"}, + {file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524"}, + {file = "pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986"}, + {file = "pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c"}, + {file = "pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3"}, + {file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af"}, + {file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f"}, + {file = "pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642"}, + {file = "pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd"}, + {file = "pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e"}, + {file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0"}, + {file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb"}, + {file = "pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f"}, + {file = "pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15"}, + {file = "pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f"}, + {file = "pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8"}, + {file = "pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586"}, + {file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce"}, + {file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8"}, + {file = "pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36"}, + {file = "pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b"}, + {file = "pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e"}, + {file = "pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +test-arrow = ["arro3-compute", "arro3-core", "nanoarrow", "pyarrow"] +tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma (>=5)", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] +xmp = ["defusedxml"] + [[package]] name = "pkginfo" version = "1.12.1.2" @@ -1421,14 +1614,14 @@ hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] [[package]] name = "pyinstaller-hooks-contrib" -version = "2026.1" +version = "2026.2" description = "Community maintained hooks for PyInstaller" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "pyinstaller_hooks_contrib-2026.1-py3-none-any.whl", hash = "sha256:66ad4888ba67de6f3cfd7ef554f9dd1a4389e2eb19f84d7129a5a6818e3f2180"}, - {file = "pyinstaller_hooks_contrib-2026.1.tar.gz", hash = "sha256:a5f0891a1e81e92406ab917d9e76adfd7a2b68415ee2e35c950a7b3910bc361b"}, + {file = "pyinstaller_hooks_contrib-2026.2-py3-none-any.whl", hash = "sha256:fc29f0481b58adf78ce9c1d9cf135fe96f38c708f74b2aa0670ef93e59578ab9"}, + {file = "pyinstaller_hooks_contrib-2026.2.tar.gz", hash = "sha256:cbd1eb00b5d13301b1cce602e1fffb17f0c531c0391f0a87a383d376be68a186"}, ] [package.dependencies] @@ -1524,14 +1717,14 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.5.4)", "pytest (>=8.3.5)", "pyt [[package]] name = "python-dotenv" -version = "1.2.1" +version = "1.2.2" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61"}, - {file = "python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6"}, + {file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"}, + {file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"}, ] [package.extras] @@ -1729,6 +1922,41 @@ files = [ [package.extras] all = ["numpy"] +[[package]] +name = "rdkit" +version = "2025.9.6" +description = "A collection of chemoinformatics and machine-learning software written in C++ and Python" +optional = true +python-versions = "*" +groups = ["main"] +markers = "extra == \"rdkit\"" +files = [ + {file = "rdkit-2025.9.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce558b4f8e63a52ec932b59e867a8e1b2e3cde304e17b652cd54e4f73a6f5607"}, + {file = "rdkit-2025.9.6-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:5aedf8a713998553e17b75a09990561a7824a542c85b48987d9bc88dbd1329f0"}, + {file = "rdkit-2025.9.6-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6d6746cea16ce048259bf463a5beef92cf130fd0f8f62a999d4eb28384e7831a"}, + {file = "rdkit-2025.9.6-cp310-cp310-win_amd64.whl", hash = "sha256:a961a99f91f1d4366d8eaf53e0648c36e3f6dd96a040be9fc6b90b4a7133dae8"}, + {file = "rdkit-2025.9.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8348ec9c5a223d94fa848f8330a68f746d870f61540a846fe8ea356998ea77f1"}, + {file = "rdkit-2025.9.6-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:d0ec921fee7f2908e1da8c4ea1185dbb6d0a22787bcf9749b442691c121c02ef"}, + {file = "rdkit-2025.9.6-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:3f4fc084890efb29b51ea4679bb07d28b276b6e73e3381e678a5ba057b4c4222"}, + {file = "rdkit-2025.9.6-cp311-cp311-win_amd64.whl", hash = "sha256:8c8e592948f442d2ec5a34f2e31295e03778686cec459a3d23c41b46ac56a358"}, + {file = "rdkit-2025.9.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:95350eeb5d70c5576eca748caf59106ebeea259278ff05c3812adb12c5c4dc10"}, + {file = "rdkit-2025.9.6-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:7e8cda5243d586356c2a66bdf70fea3cb3fd552200556efe375da64efb047f73"}, + {file = "rdkit-2025.9.6-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6be95b735c428ba27badf39e92078133d3a505f872d0a31467ba577d73c57676"}, + {file = "rdkit-2025.9.6-cp312-cp312-win_amd64.whl", hash = "sha256:80c1fe3d1cc88540f2495272f32f02b4ec58a73db53649c9e4249087deeb62dc"}, + {file = "rdkit-2025.9.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0dee75188cae572bc2cf5b580291af0f3dc36ca6bb3f7c2a395b04450507efb0"}, + {file = "rdkit-2025.9.6-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:fedb677e69e581dae2564b205418a22d1960e816a15378bf0ab358ed9ca2f477"}, + {file = "rdkit-2025.9.6-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:81023b0c9a138f0210716047ab02e4817c28d6505dc04bee783553282da06944"}, + {file = "rdkit-2025.9.6-cp313-cp313-win_amd64.whl", hash = "sha256:95572cd31ae6f57b9c77a25e904564bc4b973d0776614e65d89f22e0183552fd"}, + {file = "rdkit-2025.9.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8710d903c4a83a6db2f0423f81d031a0a12c6c500d1482268e9ff86f87f5b07c"}, + {file = "rdkit-2025.9.6-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:e90b33e5b44517b4f37d39aa2cc2780837cea6457fe23ef872a16235c81e91c8"}, + {file = "rdkit-2025.9.6-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:97fa110972724cb465714395b001518ca4aef0d7f5b42848c26d77127d16aeab"}, + {file = "rdkit-2025.9.6-cp314-cp314-win_amd64.whl", hash = "sha256:7448bc1498aff93a372728916546702994feb26838c8ce34bfc415ca552d2023"}, +] + +[package.dependencies] +numpy = "*" +Pillow = "*" + [[package]] name = "requests" version = "2.32.5" @@ -2085,8 +2313,9 @@ cffi = ["cffi (>=1.17,<2.0) ; platform_python_implementation != \"PyPy\" and pyt [extras] gui = ["pyperclip"] +rdkit = ["rdkit"] [metadata] lock-version = "2.1" python-versions = ">=3.12,<3.15" -content-hash = "29c0863bfefcb10cd0abfe597cd27281c5e3149ffbbe502e0fed03241e73826c" +content-hash = "f9b15c3b9031dbde28676ba432b90dd4ed02ce104ecdcccd0fa24adf51c56140" diff --git a/pyproject.toml b/pyproject.toml index 3b86841..5b9077e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "pythonQEPest" -version = "2.0.0a1" -description = "Java QEPest in Python" +version = "2.0.0a2" +description = "Java QEPest but written in Python" readme = "README.md" requires-python = ">=3.12,<3.15" authors = [{ name = "Lina", email = "knocker767@gmail.com" }] @@ -9,6 +9,7 @@ dependencies = ["pydantic==2.12.5", "python-dotenv>=1.2.1,<2.0.0"] [project.optional-dependencies] gui = ["pyperclip>=1.11.0,<2.0.0"] +rdkit = ["rdkit (>=2024.3.1,<2026.0.0)"] [project.scripts] pythonqepest = "pythonQEPest.cli.cli:main" diff --git a/pythonQEPest/cli/cli.py b/pythonQEPest/cli/cli.py index 80a01ca..0e99c59 100644 --- a/pythonQEPest/cli/cli.py +++ b/pythonQEPest/cli/cli.py @@ -60,7 +60,14 @@ def build_parser() -> argparse.ArgumentParser: "-f", "--format", default="txt", - help="Format to output file with " + "QEPest (json, txt).", + help="Format to output file with QEPest (json, txt).", + ) + + parser.add_argument( + "--smiles", + action="store_true", + help="Input file contains SMILES strings " + "(one per line) instead of descriptors.", ) return parser @@ -82,7 +89,10 @@ def main(argv: Sequence[str] | None = None) -> int: service = QEPestFileService( qepest=QEPest(), qepest_file=QEPestFile( - input_file=args.input, output_file=args.output, format=args.format + input_file=args.input, + output_file=args.output, + format=args.format, + smiles=args.smiles, ), ) diff --git a/pythonQEPest/config/__init__.py b/pythonQEPest/config/__init__.py index 1f050c1..aae715d 100644 --- a/pythonQEPest/config/__init__.py +++ b/pythonQEPest/config/__init__.py @@ -1,7 +1,11 @@ -from .qepest_default import qepest_default -from .normalise import normalise_default +from pythonQEPest.config.qepest_default import qepest_default +from pythonQEPest.config.normalise import normalise_default +from pythonQEPest.config.qepest_config import QEPestConfig +from pythonQEPest.config.config_provider import ConfigProvider __all__ = [ "normalise_default", "qepest_default", + "ConfigProvider", + "QEPestConfig", ] diff --git a/pythonQEPest/config/config_provider.py b/pythonQEPest/config/config_provider.py new file mode 100644 index 0000000..8e5ad0a --- /dev/null +++ b/pythonQEPest/config/config_provider.py @@ -0,0 +1,9 @@ +from abc import ABC, abstractmethod + +from pythonQEPest.config.qepest_config import QEPestConfig + + +class ConfigProvider(ABC): + @abstractmethod + def load(self) -> QEPestConfig: + pass diff --git a/pythonQEPest/config/qepest_config.py b/pythonQEPest/config/qepest_config.py new file mode 100644 index 0000000..0c19bd7 --- /dev/null +++ b/pythonQEPest/config/qepest_config.py @@ -0,0 +1,31 @@ +from typing import Optional + +from pydantic import BaseModel + +from pythonQEPest.dto.pest_type.PestTypeConfig import PestTypeConfig + + +class QEPestConfig(BaseModel): + name: Optional[str] = "HerbInsectFung" + pest_types: list[PestTypeConfig] + + def get_pest_names(self) -> list[str]: + return [pest.name for pest in self.pest_types] + + def get_coefficients( + self, pest_name: str + ) -> list[tuple[float, float, float, float]]: + for pest in self.pest_types: + if pest.name == pest_name: + return pest.coefficients.as_set() + + raise ValueError(f"Pest type '{pest_name}' not found") + + def get_normaliser( + self, pest_name: str + ) -> tuple[float, float, float, float, float, float]: + for pest in self.pest_types: + if pest.name == pest_name: + return pest.normaliser + + raise ValueError(f"Pest type '{pest_name}' not found") diff --git a/pythonQEPest/core/qepest.py b/pythonQEPest/core/qepest.py index f0716ef..0990b2c 100644 --- a/pythonQEPest/core/qepest.py +++ b/pythonQEPest/core/qepest.py @@ -1,10 +1,12 @@ import logging import math -from typing import Optional, List - -from pydantic import create_model, BaseModel +from typing import Optional +from pythonQEPest.config import ConfigProvider from pythonQEPest.core.qepest_meta import QEPestMeta +from pythonQEPest.config.qepest_config import QEPestConfig +from pythonQEPest.dto import QEPestData + from pythonQEPest.dto.QEPestInput import QEPestInput from pythonQEPest.dto.QEPestOutput import QEPestOutput from pythonQEPest.dto.normalisation.Normaliser import Normaliser @@ -12,38 +14,45 @@ from pythonQEPest.helpers.compute_df import compute_df from pythonQEPest.helpers.get_values_from_line import get_values_from_line from pythonQEPest.helpers.round_to_4digs import round_to_4digs +from pythonQEPest.providers import DefaultConfigProvider logger = logging.getLogger(__name__) class QEPest(QEPestMeta): + def __init__(self, provider: Optional[ConfigProvider] = None, *args, **kwargs): + super().__init__(*args, **kwargs) - def __init__(self, *args, **kwargs): - self.coefficients_names = None - self.names = None + self.config: QEPestConfig + self.normalisers: dict[str, Normaliser] = {} + self.qex: QEPestData = QEPestData({}) - logger.debug("QEPest initialisation") + logger.debug("QEPest initialization") - logger.info(f"QEPest args: {args}") - logger.info(f"QEPest kwargs: {kwargs}") + self.initialise_config(provider) - super().__init__(*args, **kwargs) + logger.debug("QEPest initialization successful") - logger.debug("QEPest initialisation successful") + def initialise_config(self, provider: Optional[ConfigProvider] = None): + logger.debug("Config initialisation") + if provider is None: + logger.debug("Config is empty. Loading default one.") + provider = DefaultConfigProvider() - def _log_compute_df(self, func, index, lst, data_lst) -> float: + self.config = provider.load() + logger.debug(f"Config {self.config} loaded") + + for pest in self.config.pest_types: + self.normalisers[pest.name] = Normaliser(pest.normaliser) + + def _log_compute_df( + self, func, index: int, lst: list[float], data_lst: list + ) -> float: df_result = compute_df(lst[index], *data_lst[index]) return math.log(func(df_result, index)) - def get_names(self) -> List[str]: - self.names = [ - n.split("_")[1] for n in dir(self) if n.startswith("coefficient_") - ] - return self.names - - def get_coefficients_names(self) -> List[str]: - self.coefficients_names = [f"coefficient_{name}" for name in self.names] - return self.coefficients_names + def get_names(self) -> list[str]: + return self.config.get_pest_names() def compute_params(self, data_input: QEPestInput) -> QEPestOutput: self.get_qex_values( @@ -51,76 +60,39 @@ def compute_params(self, data_input: QEPestInput) -> QEPestOutput: ) return QEPestOutput(data=self.qex, name=data_input.name) - def get_qex_values(self, d) -> BaseModel: + def get_qex_values(self, d: list[float]) -> QEPestData: names = self.get_names() - # Coefficients names = ("coefficients_fung, coefficient_herb...) - coefficients_names = self.get_coefficients_names() - - if len(coefficients_names) == 0: + if len(names) == 0: raise ValueError( - "No coefficient_ keys, needs to call " - + "initialize_coefficient and workable config to work" + "No pest types configured. " "Please provide a valid configuration." ) - # coefficients = [{i: getattr(self, i)} for i in coefficients_names] - - # Splitting by _ to get (fung, herb, etc...) to form qe_fung, qe_herb... - # qe_lst = [] - for z in names: - name = f"qe_{z}" - setattr(self, name, 0.0) + qe_values = dict.fromkeys(names, 0.0) d_num = len(d) for i in range(d_num): for name in names: - self.__dict__[f"qe_{name}"] += self._log_compute_df( - func=self.__dict__[f"normaliser_{name}"].norm, + coeffs = self.config.get_coefficients(name) + normaliser = self.normalisers[name] + qe_values[name] += self._log_compute_df( + func=normaliser.norm, index=i, lst=d, - data_lst=self.__dict__[f"coefficient_{name}"], + data_lst=coeffs, ) - q = [ - round_to_4digs(math.exp(self.__dict__[f"qe_{name}"] / d_num)) - for name in names - ] + result = [round_to_4digs(math.exp(qe_values[name] / d_num)) for name in names] - result = check_nan(q) + result = check_nan(result) - fields = {f"qe_{name}": (float, 0.0) for name in names} - dynamic_model = create_model("QEPestData", **fields) - self.qex = dynamic_model( - **{f"qe_{names[idx]}": name for idx, name in enumerate(result)} + self.qex = QEPestData( + {f"qe_{name}": result[idx] for idx, name in enumerate(names)} ) return self.qex - # TODO: Coefficients must be in other class. - # TODO: Ability to provide whatever we want is a good thingy - def initialize_coefficients(self, coefficients: Optional = None) -> None: - logger.debug("QEPest coefficients initialisation") - coefficients = super().initialize_coefficients() - - logger.debug("QEPest coefficients initialisation successful") - for category, data in coefficients.items(): - setattr(self, f"coefficient_{category}", data) - - logger.info(f"QEPest coefficients initialisation with {coefficients.items()}") - - # TODO: Same with Normalisers - def initialize_normalisers(self, normalisers: Optional = None) -> None: - logger.debug("QEPest normalisers initialisation") - normalisers = super().initialize_normalisers() - - logger.debug("QEPest normalisers initialisation successful") - - for category, data in normalisers.items(): - setattr(self, f"normaliser_{category}", Normaliser(data)) - - logger.info(f"QEPest normalisers initialisation with {normalisers.items()}") - if __name__ == "__main__": qepest = QEPest() - qepest.get_qex_values(1) + qepest.get_qex_values([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]) diff --git a/pythonQEPest/core/qepest_meta.py b/pythonQEPest/core/qepest_meta.py index a808daf..5728c55 100644 --- a/pythonQEPest/core/qepest_meta.py +++ b/pythonQEPest/core/qepest_meta.py @@ -13,29 +13,16 @@ def __init__(self, *args, **kwargs): self.col_number: int = 7 self.dir: str = os.getcwd() - self.initialize_coefficients() - self.initialize_normalisers() - @abstractmethod def compute_params(self, data_input: QEPestInput) -> QEPestOutput: pass @abstractmethod - def get_qex_values(self, d) -> None: + def get_qex_values(self, d: list[float]) -> None: pass @abstractmethod - def initialize_coefficients(self, coefficients=None) -> None: - if coefficients is None: - from pythonQEPest.config.qepest_default import qepest_default - - coefficients = qepest_default - return coefficients - - @abstractmethod - def initialize_normalisers(self, normalisers=None) -> None: - if normalisers is None: - from pythonQEPest.config.normalise import normalise_default - - normalisers = normalise_default - return normalisers + def _log_compute_df( + self, func, index: int, lst: list[float], data_lst: list + ) -> float: + pass diff --git a/pythonQEPest/dto/QEPestData.py b/pythonQEPest/dto/QEPestData.py index 5445613..8392d77 100644 --- a/pythonQEPest/dto/QEPestData.py +++ b/pythonQEPest/dto/QEPestData.py @@ -1,7 +1,21 @@ -from pydantic import BaseModel +from pydantic import RootModel, model_validator -class QEPestData(BaseModel): - qe_h: float - qe_i: float - qe_f: float +class QEPestData(RootModel[dict[str, float]]): + @model_validator(mode="before") + @classmethod + def handle_input(cls, data): + if data is None: + return {} + if isinstance(data, dict): + return data + return data + + def __getattr__(self, name: str) -> float: + return self.root.get(name, 0.0) + + def __setattr__(self, name: str, value: float) -> None: + if name == "root": + super().__setattr__(name, value) + else: + self.root[name] = value diff --git a/pythonQEPest/dto/QEPestFile.py b/pythonQEPest/dto/QEPestFile.py index 5ccf5c7..e43b7f3 100644 --- a/pythonQEPest/dto/QEPestFile.py +++ b/pythonQEPest/dto/QEPestFile.py @@ -13,6 +13,7 @@ class QEPestFile(BaseModel): input_file: Optional[str] = None output_file: Optional[str] = None format: Optional[QEPestFormat] = QEPestFormat.TXT + smiles: Optional[bool] = False model_config = {"arbitrary_types_allowed": True} diff --git a/pythonQEPest/dto/QEPestInput.py b/pythonQEPest/dto/QEPestInput.py index 0811f26..2c526a8 100644 --- a/pythonQEPest/dto/QEPestInput.py +++ b/pythonQEPest/dto/QEPestInput.py @@ -1,7 +1,10 @@ +import logging from typing import Union from pydantic import BaseModel +logger = logging.getLogger(__name__) + class QEPestInput(BaseModel): name: str = "" @@ -28,3 +31,30 @@ def from_array(cls, data: Union[list, tuple, set]): rotatable_bonds=int(data[5]), aromatic_rings=int(data[6]), ) + + @classmethod + def from_smiles(cls, smiles: str, name: str = ""): + try: + from rdkit import Chem + from rdkit.Chem import Descriptors + except ImportError: + logger.warning( + "RDKit is not installed. Install it with: poetry install --with rdkit. " + "Returning QEPestInput with default values." + ) + return cls(name=name) + + mol = Chem.MolFromSmiles(smiles) + + if mol is None: + raise ValueError(f"Invalid SMILES: {smiles}") + + return cls( + name=name, + mol_weight=Descriptors.ExactMolWt(mol), + log_p=Descriptors.MolLogP(mol), + hbond_acceptors=Descriptors.NumHAcceptors(mol), + hbond_donors=Descriptors.NumHDonors(mol), + rotatable_bonds=Descriptors.NumRotatableBonds(mol), + aromatic_rings=Descriptors.NumAromaticRings(mol), + ) diff --git a/pythonQEPest/dto/QEPestOutput.py b/pythonQEPest/dto/QEPestOutput.py index d5415b2..ef867fa 100644 --- a/pythonQEPest/dto/QEPestOutput.py +++ b/pythonQEPest/dto/QEPestOutput.py @@ -1,11 +1,13 @@ from typing import Optional from pydantic import BaseModel +from pythonQEPest.dto.QEPestData import QEPestData class QEPestOutput(BaseModel): - data: BaseModel + data: QEPestData name: Optional[str] = "" def to_array(self) -> list: - return [self.name] + list(self.data.model_dump().values()) + values = list(self.data.root.values()) + return [self.name] + values diff --git a/pythonQEPest/dto/__init__.py b/pythonQEPest/dto/__init__.py index 3db3c11..e60e523 100644 --- a/pythonQEPest/dto/__init__.py +++ b/pythonQEPest/dto/__init__.py @@ -3,4 +3,25 @@ from pythonQEPest.dto.QEPestFile import QEPestFile, QEPestFormat from pythonQEPest.dto.QEPestOutput import QEPestOutput -__all__ = ["QEPestOutput", "QEPestData", "QEPestInput", "QEPestFile", "QEPestFormat"] +from pythonQEPest.dto.coefficients import QEPestCoefficient +from pythonQEPest.dto.coefficients import QEPestCoefficientList +from pythonQEPest.dto.coefficients import QEPestCoefficientNumerics + +from pythonQEPest.dto.normalisation import Normaliser + +from pythonQEPest.dto.pest_type.PestTypeCoefficient import PestTypeCoefficient +from pythonQEPest.dto.pest_type.PestTypeConfig import PestTypeConfig + +__all__ = [ + "QEPestOutput", + "QEPestData", + "QEPestInput", + "QEPestFile", + "QEPestFormat", + "PestTypeConfig", + "PestTypeCoefficient", + "Normaliser", + "QEPestCoefficientNumerics", + "QEPestCoefficientList", + "QEPestCoefficient", +] diff --git a/pythonQEPest/dto/pest_type/PestTypeCoefficient.py b/pythonQEPest/dto/pest_type/PestTypeCoefficient.py new file mode 100644 index 0000000..ea4c6e7 --- /dev/null +++ b/pythonQEPest/dto/pest_type/PestTypeCoefficient.py @@ -0,0 +1,30 @@ +from typing import Any + +from pydantic import BaseModel, model_validator + + +class PestTypeCoefficient(BaseModel): + mwH: tuple[float, float, float, float] + logpH: tuple[float, float, float, float] + hbaH: tuple[float, float, float, float] + hbdH: tuple[float, float, float, float] + rbH: tuple[float, float, float, float] + arRCH: tuple[float, float, float, float] + + @model_validator(mode="before") + @classmethod + def accept_positional(cls, data: Any): + if isinstance(data, dict): + return data + + if isinstance(data, (list, tuple)): + if len(data) != 6: + raise ValueError("Expected 6 items: mwH, logpH, hbaH, hbdH, rbH, arRCH") + + keys = ("mwH", "logpH", "hbaH", "hbdH", "rbH", "arRCH") + return dict(zip(keys, data)) + + raise TypeError("Expected a dict or a 6-item tuple/list") + + def as_set(self): + return [self.mwH, self.logpH, self.hbaH, self.hbdH, self.rbH, self.arRCH] diff --git a/pythonQEPest/dto/pest_type/PestTypeConfig.py b/pythonQEPest/dto/pest_type/PestTypeConfig.py new file mode 100644 index 0000000..22c9630 --- /dev/null +++ b/pythonQEPest/dto/pest_type/PestTypeConfig.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel + +from pythonQEPest.dto.pest_type.PestTypeCoefficient import PestTypeCoefficient + + +class PestTypeConfig(BaseModel): + name: str + coefficients: PestTypeCoefficient + normaliser: tuple[float, float, float, float, float, float] diff --git a/pythonQEPest/dto/pest_type/__init__.py b/pythonQEPest/dto/pest_type/__init__.py new file mode 100644 index 0000000..602d27a --- /dev/null +++ b/pythonQEPest/dto/pest_type/__init__.py @@ -0,0 +1,4 @@ +from pythonQEPest.dto.pest_type.PestTypeCoefficient import PestTypeCoefficient +from pythonQEPest.dto.pest_type.PestTypeConfig import PestTypeConfig + +__all__ = ["PestTypeConfig", "PestTypeCoefficient"] diff --git a/pythonQEPest/providers/__init__.py b/pythonQEPest/providers/__init__.py new file mode 100644 index 0000000..fa2d53a --- /dev/null +++ b/pythonQEPest/providers/__init__.py @@ -0,0 +1,6 @@ +from pythonQEPest.providers.default_provider import DefaultConfigProvider + +__all__ = [ + "DefaultConfigProvider", + "JSONConfigProvider", +] diff --git a/pythonQEPest/providers/default_provider.py b/pythonQEPest/providers/default_provider.py new file mode 100644 index 0000000..cef2d81 --- /dev/null +++ b/pythonQEPest/providers/default_provider.py @@ -0,0 +1,23 @@ +from pythonQEPest.config.config_provider import ConfigProvider +from pythonQEPest.config.qepest_config import QEPestConfig, PestTypeConfig +from pythonQEPest.dto import PestTypeCoefficient + + +class DefaultConfigProvider(ConfigProvider): + def load(self) -> QEPestConfig: + from pythonQEPest.config.qepest_default import qepest_default + from pythonQEPest.config.normalise import normalise_default + + pest_types = [] + for pest_name in qepest_default.keys(): + pest_types.append( + PestTypeConfig( + name=pest_name, + coefficients=PestTypeCoefficient.model_validate( + qepest_default[pest_name] + ), + normaliser=normalise_default[pest_name], + ) + ) + + return QEPestConfig(pest_types=pest_types) diff --git a/pythonQEPest/providers/json_provider.py b/pythonQEPest/providers/json_provider.py new file mode 100644 index 0000000..00ce2d1 --- /dev/null +++ b/pythonQEPest/providers/json_provider.py @@ -0,0 +1,30 @@ +import json +from pathlib import Path + +from pythonQEPest.config.config_provider import ConfigProvider +from pythonQEPest.config.qepest_config import QEPestConfig, PestTypeConfig + + +class JSONConfigProvider(ConfigProvider): + def load(self, path: str | Path) -> QEPestConfig: + path = Path(path) + + with open(path, encoding="utf-8") as f: + data = json.load(f) + + return self.load_raw_json(data) + + def load_raw_json(self, data): + pest_types = [] + for pest_name, pest_data in data.items(): + coefficients = [tuple(coef) for coef in pest_data["coefficients"]] + normaliser = tuple(pest_data["normaliser"]) + pest_types.append( + PestTypeConfig( + name=pest_name, + coefficients=coefficients, + normaliser=normaliser, + ) + ) + + return QEPestConfig(pest_types=pest_types) diff --git a/pythonQEPest/services/QEPestFileService.py b/pythonQEPest/services/QEPestFileService.py index 2796b2e..b862d9a 100644 --- a/pythonQEPest/services/QEPestFileService.py +++ b/pythonQEPest/services/QEPestFileService.py @@ -2,7 +2,7 @@ import json from pythonQEPest.core import QEPestMeta -from pythonQEPest.dto import QEPestFile +from pythonQEPest.dto import QEPestFile, QEPestInput from pythonQEPest.dto.QEPestFile import QEPestFormat from pythonQEPest.helpers.get_values_from_line import get_values_from_line from pythonQEPest.helpers.get_num_of_cols import get_num_of_cols @@ -30,7 +30,66 @@ def _write_json_line(self, line: str, file) -> None: data = {"name": splitted_line, **self.qepest.qex.model_dump()} file.write(json.dumps(data) + "\n") + def _process_smiles_line(self, smiles: str, index: int) -> None: + qepest_input = QEPestInput.from_smiles(smiles, name=f"mol_{index}") + if qepest_input.mol_weight == 0.0: + raise RuntimeError( + "RDKit is not installed. Install it with: pip install rdkit" + ) + d_values = get_values_from_line(list(qepest_input.model_dump().values())) + self.qepest.get_qex_values(d_values) + + def _write_smiles_txt_line(self, index: int, file) -> None: + name = f"mol_{index}" + qex_headers = " ".join(self.qepest.qex.model_dump().keys()) + qex_values = " ".join(str(x) for x in self.qepest.qex.model_dump().values()) + file.write(f"Name {qex_headers.upper()}\n") + file.write(f"{name} {qex_values}\n") + + def _write_smiles_json_line(self, index: int, file) -> None: + name = f"mol_{index}" + data = {"name": name, **self.qepest.qex.model_dump()} + file.write(json.dumps(data) + "\n") + def read_file_and_compute_params(self) -> None: + if self.qepest_file.smiles: + self._read_smiles_file() + else: + self._read_descriptor_file() + + def _read_smiles_file(self) -> None: + try: + with open(self.qepest_file.input_file, "r") as f: + lines = f.readlines() + + with open(self.qepest_file.output_file, "w") as wr: + for index, line in enumerate(lines): + smiles = line.strip() + if not smiles: + continue + + try: + self._process_smiles_line(smiles, index) + except RuntimeError as e: + logger.error(str(e)) + self.error = True + break + + if self.qepest_file.format == QEPestFormat.TXT: + self._write_smiles_txt_line(index, wr) + else: + self._write_smiles_json_line(index, wr) + + if not self.error: + logger.info("Computation completed") + else: + logger.warning("Finished with errors") + + except FileNotFoundError: + self.error = True + logger.error(f"Error: can't find: {self.qepest_file.input_file}") + + def _read_descriptor_file(self) -> None: try: with open(self.qepest_file.input_file, "r") as f: lines = f.readlines() diff --git a/tests/cli/test_cli_contract.py b/tests/cli/test_cli_contract.py index da70cc9..f251da7 100644 --- a/tests/cli/test_cli_contract.py +++ b/tests/cli/test_cli_contract.py @@ -38,7 +38,7 @@ def test_cli_runs_with_explicit_input_file(self, monkeypatch, tmp_path): output_text = output_file.read_text(encoding="utf-8") assert output_file.name == "input.out.txt" assert ( - output_text == "Name QE_FUNG QE_HERB QE_INSECT\nmol1 0.6224 0.8511 0.5339\n" + output_text == "Name QE_HERB QE_INSECT QE_FUNG\nmol1 0.8511 0.5339 0.6224\n" ) def test_cli_runs_with_explicit_input_file_format(self, monkeypatch, tmp_path): @@ -66,7 +66,7 @@ def test_cli_runs_with_explicit_input_file_format(self, monkeypatch, tmp_path): assert isinstance(output_text, str) assert ( - output_text == "Name QE_FUNG QE_HERB QE_INSECT\nmol1 0.6224 0.8511 0.5339\n" + output_text == "Name QE_HERB QE_INSECT QE_FUNG\nmol1 0.8511 0.5339 0.6224\n" ) def test_cli_runs_with_input_file_format(self, monkeypatch, tmp_path): diff --git a/tests/cli/test_cli_smiles.py b/tests/cli/test_cli_smiles.py new file mode 100644 index 0000000..dd79ed1 --- /dev/null +++ b/tests/cli/test_cli_smiles.py @@ -0,0 +1,56 @@ +import pytest + +from pythonQEPest.cli.cli import main + +rdkit = pytest.importorskip("rdkit") + + +class TestCLISmiles: + def test_cli_smiles_txt(self, monkeypatch, tmp_path): + monkeypatch.setenv("APP_DEBUG_ENABLE", "false") + + data_file = tmp_path / "input.txt" + output_file = tmp_path / "input.out.txt" + + data_file.write_text( + "C1=CC(=NC(=C1Cl)C(=O)O)Cl", + encoding="utf-8", + ) + + exit_code = main(["--input", str(data_file), "--smiles"]) + + assert exit_code == 0 + assert output_file.exists() + + output_text = output_file.read_text(encoding="utf-8") + assert ( + output_text + == "Name QE_HERB QE_INSECT QE_FUNG\nmol_0 0.7258 0.5072 0.6809\n" + ) + + def test_cli_smiles_json(self, monkeypatch, tmp_path): + monkeypatch.setenv("APP_DEBUG_ENABLE", "false") + + data_file = tmp_path / "input.txt" + output_file = tmp_path / "input.out.json" + + data_file.write_text( + "C1=CC(=NC(=C1Cl)C(=O)O)Cl", + encoding="utf-8", + ) + + exit_code = main(["--input", str(data_file), "--smiles", "--format=json"]) + + assert exit_code == 0 + assert output_file.exists() + + output_text = output_file.read_text(encoding="utf-8") + + import json + + assert json.loads(output_text) == { + "name": "mol_0", + "qe_fung": 0.6809, + "qe_herb": 0.7258, + "qe_insect": 0.5072, + } diff --git a/tests/config/test_change_configs.py b/tests/config/test_change_configs.py new file mode 100644 index 0000000..629eb35 --- /dev/null +++ b/tests/config/test_change_configs.py @@ -0,0 +1,179 @@ +from pythonQEPest.config.config_provider import ConfigProvider +from pythonQEPest.config.qepest_config import QEPestConfig, PestTypeConfig +from pythonQEPest.core.qepest import QEPest +from pythonQEPest.dto import PestTypeCoefficient +from pythonQEPest.dto import QEPestInput + + +def random_coefficients() -> list: + return [ + (53.506563385007496, 222.26815379980897, 14.19499523870596, 272.5424991172166), + (46.00715643037571, 350.16089820262516, 208.06156087128122, 492.8206056952804), + (138.64460966233446, 241.53026279945124, 377.8993212072419, 174.0669648564985), + (180.6998401258752, 323.1726892417753, 485.14617786028015, 443.1012676802161), + (215.3285314998089, 361.68177460255873, 342.7354563068766, 192.3337134546996), + (125.13208437424792, 48.40447428920039, 440.66537907336544, 38.00801457055535), + ] + + +def random_normaliser() -> tuple: + return ( + 259.82936625344365, + 269.23065237533837, + 73.92347759714227, + 328.30992675910346, + 457.99577359338286, + 388.29035194219455, + ) + + +COEFFICIENTS = [ + (10.0, 20.0, 30.0, 40.0), + (15.0, 25.0, 35.0, 45.0), + (12.0, 22.0, 32.0, 42.0), + (18.0, 28.0, 38.0, 48.0), + (11.0, 21.0, 31.0, 41.0), + (14.0, 24.0, 34.0, 44.0), +] + +NORMALISERS = (100.0, 100.0, 100.0, 100.0, 100.0, 100.0) + + +class SinglePestProvider(ConfigProvider): + def __init__(self, coeffs, normalisers): + # (100.0, 100.0, 100.0, 100.0, 100.0, 100.0) + self.coeffs = coeffs + self.normalisers = normalisers + + def load(self) -> QEPestConfig: + return QEPestConfig( + pest_types=[ + PestTypeConfig( + name="custom", + coefficients=PestTypeCoefficient.model_validate(self.coeffs), + normaliser=self.normalisers, + ) + ] + ) + + +class CustomTestConfigProvider(ConfigProvider): + def __init__(self): + self._config = self._build_config() + + def _build_config(self) -> QEPestConfig: + pest_types = [] + for pest_name in ["herb", "insect", "fung"]: + pest_types.append( + PestTypeConfig( + name=pest_name, + coefficients=PestTypeCoefficient.model_validate( + random_coefficients() + ), + normaliser=random_normaliser(), + ) + ) + return QEPestConfig(pest_types=pest_types) + + def load(self) -> QEPestConfig: + return self._config + + +class TestCustomConfig: + + def make_input(self, name="test", mw=120.5, logp=3.1, hba=2, hbd=1, rb=4, ar=1): + return QEPestInput( + name=name, + mol_weight=mw, + log_p=logp, + hbond_acceptors=hba, + hbond_donors=hbd, + rotatable_bonds=rb, + aromatic_rings=ar, + ) + + def test_custom_config_produces_different_results(self): + qepest_default = QEPest() + qepest_custom = QEPest(provider=CustomTestConfigProvider()) + + inp = self.make_input("mol1", 240.2127, 3.2392, 5, 1, 4, 1) + + result_default = qepest_default.compute_params(inp) + + assert result_default.to_array() == ["mol1", 0.8511, 0.5339, 0.6224] + + result_custom = qepest_custom.compute_params(inp) + + assert result_custom.to_array() == ["mol1", 1.2516, 1.2516, 1.2516] + + assert result_default.data.qe_herb != result_custom.data.qe_herb + assert result_default.data.qe_insect != result_custom.data.qe_insect + assert result_default.data.qe_fung != result_custom.data.qe_fung + + def test_custom_config_same_seed_produces_consistent_results(self): + provider1 = CustomTestConfigProvider() + provider2 = CustomTestConfigProvider() + + qepest1 = QEPest(provider=provider1) + qepest2 = QEPest(provider=provider2) + + inp = self.make_input("test", 200.0, 2.5, 3, 2, 5, 1) + + result1 = qepest1.compute_params(inp) + result2 = qepest2.compute_params(inp) + + assert result1.data.qe_herb == result2.data.qe_herb + assert result1.data.qe_insect == result2.data.qe_insect + assert result1.data.qe_fung == result2.data.qe_fung + + def test_custom_config_with_single_pest_type(self): + qepest = QEPest( + provider=SinglePestProvider(coeffs=COEFFICIENTS, normalisers=NORMALISERS) + ) + inp = self.make_input("test", 100.0, 1.0, 1, 1, 1, 1) + + result = qepest.compute_params(inp) + + assert hasattr(result.data, "qe_custom") + assert qepest.get_names() == ["custom"] + + def test_change_configs(self): + qepest_default = QEPest() + + inp = self.make_input("mol1", 240.2127, 3.2392, 5, 1, 4, 1) + + result_default = qepest_default.compute_params(inp) + + assert result_default.to_array() == ["mol1", 0.8511, 0.5339, 0.6224] + + qepest_default.initialise_config(CustomTestConfigProvider()) + + results_after_changing = qepest_default.compute_params(inp) + + assert results_after_changing.to_array() == ["mol1", 1.2516, 1.2516, 1.2516] + + assert result_default.data.qe_herb != results_after_changing.data.qe_herb + assert result_default.data.qe_insect != results_after_changing.data.qe_insect + assert result_default.data.qe_fung != results_after_changing.data.qe_fung + + def test_change_configs_aggressively(self): + qepest_default = QEPest() + + inp = self.make_input("mol1", 240.2127, 3.2392, 5, 1, 4, 1) + + result_default = qepest_default.compute_params(inp) + + assert result_default.to_array() == ["mol1", 0.8511, 0.5339, 0.6224] + + assert result_default.data.qe_fung == 0.6224 + assert result_default.data.qe_insect == 0.5339 + assert result_default.data.qe_herb == 0.8511 + + qepest_default.initialise_config( + SinglePestProvider(coeffs=COEFFICIENTS, normalisers=NORMALISERS) + ) + + result_aggressive = qepest_default.compute_params(inp) + + assert result_aggressive.to_array() == ["mol1", 0.5199] + assert result_aggressive.data.qe_custom == 0.5199 diff --git a/tests/core/test_qepest.py b/tests/core/test_qepest.py index eb7fc46..3dd76ef 100644 --- a/tests/core/test_qepest.py +++ b/tests/core/test_qepest.py @@ -22,9 +22,6 @@ def make_input(self, name="test", mw=120.5, logp=3.1, hba=2, hbd=1, rb=4, ar=1): def test_qepest_compute_params_basic(self): qepest = QEPest() - qepest.initialize_coefficients() - qepest.initialize_normalisers() - inp = self.make_input() result = qepest.compute_params(inp) @@ -82,7 +79,7 @@ def test_qepest_compare_with_original(self): assert result.data.qe_insect == 0.5339 assert result.data.qe_fung == 0.6224 - assert result.to_array() == ["mol1", 0.6224, 0.8511, 0.5339] + assert result.to_array() == ["mol1", 0.8511, 0.5339, 0.6224] result = qepest.compute_params( self.make_input("mol2", 249.091, 3.0273, 3, 1, 5, 1) @@ -93,7 +90,7 @@ def test_qepest_compare_with_original(self): assert result.data.qe_insect == 0.6913 assert result.data.qe_fung == 0.731 - assert result.to_array() == ["mol2", 0.731, 0.975, 0.6913] + assert result.to_array() == ["mol2", 0.975, 0.6913, 0.731] result = qepest.compute_params( self.make_input("mol3", 308.354, 2.1086, 1, 0, 7, 1) @@ -104,7 +101,7 @@ def test_qepest_compare_with_original(self): assert result.data.qe_insect == 0.9018 assert result.data.qe_fung == 0.732 - assert result.to_array() == ["mol3", 0.732, 0.798, 0.9018] + assert result.to_array() == ["mol3", 0.798, 0.9018, 0.732] result = qepest.compute_params( self.make_input("mol4", 360.444, 4.0137, 3, 0, 8, 0) @@ -114,7 +111,7 @@ def test_qepest_compare_with_original(self): assert result.data.qe_insect == 0.8382 assert result.data.qe_fung == 0.6594 - assert result.to_array() == ["mol4", 0.6594, 0.5839, 0.8382] + assert result.to_array() == ["mol4", 0.5839, 0.8382, 0.6594] result = qepest.compute_params( self.make_input("mol5", 295.335, 4.9335, 2, 0, 1, 1) @@ -124,4 +121,4 @@ def test_qepest_compare_with_original(self): assert result.data.qe_insect == 0.8118 assert result.data.qe_fung == 0.8742 - assert result.to_array() == ["mol5", 0.8742, 0.8099, 0.8118] + assert result.to_array() == ["mol5", 0.8099, 0.8118, 0.8742] diff --git a/tests/dto/normalisation/__init__.py b/tests/dto/normalisation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/dto/test_norm.py b/tests/dto/normalisation/test_normalisation.py similarity index 92% rename from tests/dto/test_norm.py rename to tests/dto/normalisation/test_normalisation.py index 2125c1a..9a24f90 100644 --- a/tests/dto/test_norm.py +++ b/tests/dto/normalisation/test_normalisation.py @@ -1,11 +1,10 @@ import pytest -from pythonQEPest.dto.normalisation.Normaliser import Normaliser +from pythonQEPest.dto import Normaliser -# from pythonQEPest.helpers.norm import norm_i, norm_h, norm, norm_f +class TestNormalisation: -class TestNorm: def test_norm_basic(self): arr = [10, 20, 30] d = 20 diff --git a/tests/dto/test_qepest_input.py b/tests/dto/test_qepest_input.py index f2ca35b..0d9e06f 100644 --- a/tests/dto/test_qepest_input.py +++ b/tests/dto/test_qepest_input.py @@ -72,3 +72,19 @@ def test_qepest_output_functions(self): assert qepest_input.rotatable_bonds == self.rotatable_bonds assert qepest_input.aromatic_rings == self.aromatic_rings + + def test_qepest_input_smiles(self): + qepest_input = QEPestInput.from_smiles( + "C1=CC(=NC(=C1Cl)C(=O)O)Cl", name="Clopyralid" + ) + assert isinstance(qepest_input, QEPestInput) + + assert qepest_input.name == "Clopyralid" + + assert qepest_input.aromatic_rings == 1 + + assert qepest_input.hbond_donors == 1 + assert qepest_input.hbond_acceptors == 2 + + assert qepest_input.log_p == 2.0866 + assert qepest_input.mol_weight == 190.954083696 diff --git a/tests/dto/test_qepest_output.py b/tests/dto/test_qepest_output.py index c34c955..014521f 100644 --- a/tests/dto/test_qepest_output.py +++ b/tests/dto/test_qepest_output.py @@ -36,3 +36,11 @@ def test_qepest_output_functions(self): qepest_output = QEPestOutput(name=self.name, data=qepest_data) assert qepest_output.to_array() == [self.name, self.qe_h, self.qe_i, self.qe_f] + + def test_qepest_output_new_format(self): + qepest_data = QEPestData( + qe_herb=self.qe_h, qe_insect=self.qe_i, qe_fung=self.qe_f + ) + qepest_output = QEPestOutput(name=self.name, data=qepest_data) + + assert qepest_output.to_array() == [self.name, self.qe_h, self.qe_i, self.qe_f] diff --git a/tests/public_api/test_change_configs.py b/tests/public_api/test_change_configs.py new file mode 100644 index 0000000..b7e567d --- /dev/null +++ b/tests/public_api/test_change_configs.py @@ -0,0 +1,64 @@ +from pathlib import Path +from pythonQEPest.providers.json_provider import JSONConfigProvider + + +class TestChangeConfigs: + def test_load_raw_json(self): + data = { + "herb": { + "coefficients": [ + [10.0, 20.0, 30.0, 40.0], + [15.0, 25.0, 35.0, 45.0], + [12.0, 22.0, 32.0, 42.0], + [18.0, 28.0, 38.0, 48.0], + [11.0, 21.0, 31.0, 41.0], + [14.0, 24.0, 34.0, 44.0], + ], + "normaliser": [100.0, 100.0, 100.0, 100.0, 100.0, 100.0], + }, + "insect": { + "coefficients": [ + [5.0, 10.0, 15.0, 20.0], + [25.0, 30.0, 35.0, 40.0], + [45.0, 50.0, 55.0, 60.0], + [65.0, 70.0, 75.0, 80.0], + [85.0, 90.0, 95.0, 100.0], + [105.0, 110.0, 115.0, 120.0], + ], + "normaliser": [200.0, 200.0, 200.0, 200.0, 200.0, 200.0], + }, + } + + config = JSONConfigProvider().load_raw_json(data) + + assert config is not None + pest_names = config.get_pest_names() + assert "herb" in pest_names + assert "insect" in pest_names + assert len(pest_names) == 2 + + herb_coeffs = config.get_coefficients("herb") + assert len(herb_coeffs) == 6 + assert herb_coeffs[0] == (10.0, 20.0, 30.0, 40.0) + + herb_norm = config.get_normaliser("herb") + assert herb_norm == (100.0, 100.0, 100.0, 100.0, 100.0, 100.0) + + insect_norm = config.get_normaliser("insect") + assert insect_norm == (200.0, 200.0, 200.0, 200.0, 200.0, 200.0) + + def test_load_from_file(self): + config_path = Path(__file__).parent / "test_config.json" + + config = JSONConfigProvider().load(config_path) + + assert config is not None + pest_names = config.get_pest_names() + assert "herb" in pest_names + assert len(pest_names) == 1 + + herb_coeffs = config.get_coefficients("herb") + assert len(herb_coeffs) == 6 + + herb_norm = config.get_normaliser("herb") + assert herb_norm == (100.0, 100.0, 100.0, 100.0, 100.0, 100.0) diff --git a/tests/public_api/test_config.json b/tests/public_api/test_config.json new file mode 100644 index 0000000..3b4da8e --- /dev/null +++ b/tests/public_api/test_config.json @@ -0,0 +1,13 @@ +{ + "herb": { + "coefficients": [ + [10.0, 20.0, 30.0, 40.0], + [15.0, 25.0, 35.0, 45.0], + [12.0, 22.0, 32.0, 42.0], + [18.0, 28.0, 38.0, 48.0], + [11.0, 21.0, 31.0, 41.0], + [14.0, 24.0, 34.0, 44.0] + ], + "normaliser": [100.0, 100.0, 100.0, 100.0, 100.0, 100.0] + } +} diff --git a/tests/public_api/test_import_contract.py b/tests/public_api/test_import_contract.py index 9880604..2a60279 100644 --- a/tests/public_api/test_import_contract.py +++ b/tests/public_api/test_import_contract.py @@ -11,6 +11,7 @@ def test_public_api_symbols_are_importable(self): assert QEPestOutput is not None assert QEPestData is not None + # Clumsy but kinda works, lol def test_public_api_compute_smoke(self): model = QEPest() payload = QEPestInput( @@ -23,10 +24,62 @@ def test_public_api_compute_smoke(self): aromatic_rings=1, ) + payload1 = QEPestInput( + name="mol2", + mol_weight=240.354, + log_p=2.1086, + hbond_acceptors=2, + hbond_donors=2, + rotatable_bonds=4, + aromatic_rings=1, + ) + + payload2 = QEPestInput( + name="mol3", + mol_weight=250.354, + log_p=1.1086, + hbond_acceptors=2, + hbond_donors=1, + rotatable_bonds=4, + aromatic_rings=1, + ) + result = model.compute_params(payload) + result1 = model.compute_params(payload1) + result2 = model.compute_params(payload2) assert isinstance(result, QEPestOutput) + assert isinstance(result1, QEPestOutput) + assert isinstance(result2, QEPestOutput) + assert isinstance(result.data, BaseModel) + assert isinstance(result1.data, BaseModel) + assert isinstance(result2.data, BaseModel) + + assert result.name == "mol1" + assert result.data.model_dump() == { + "qe_herb": 0.9357, + "qe_insect": 0.7146, + "qe_fung": 0.8022, + } + + assert result1.name == "mol2" + assert result1.data.model_dump() == { + "qe_herb": 0.8173, + "qe_insect": 0.5564, + "qe_fung": 0.604, + } + + assert result2.name == "mol3" + assert result2.data.model_dump() == { + "qe_herb": 0.7526, + "qe_insect": 0.6518, + "qe_fung": 0.677, + } + + result = model.compute_params(payload) + result1 = model.compute_params(payload1) + result2 = model.compute_params(payload2) assert result.name == "mol1" assert result.data.model_dump() == { @@ -35,6 +88,20 @@ def test_public_api_compute_smoke(self): "qe_fung": 0.8022, } + assert result1.name == "mol2" + assert result1.data.model_dump() == { + "qe_herb": 0.8173, + "qe_insect": 0.5564, + "qe_fung": 0.604, + } + + assert result2.name == "mol3" + assert result2.data.model_dump() == { + "qe_herb": 0.7526, + "qe_insect": 0.6518, + "qe_fung": 0.677, + } + @pytest.mark.skip(reason="Need to find optional approach") def test_helpers_imports(self): try: