Skip to content

Commit 25251ab

Browse files
authored
Merge pull request #298 from ReactionMechanismGenerator/update_django
Update to Python=3.9 and update Django
2 parents e665013 + 11d3845 commit 25251ab

22 files changed

+1741
-324
lines changed

.gitignore

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,6 @@ rmgweb/media/rmg/tools/*
4444
.pydevproject
4545
.settings/*
4646

47-
# WSGI
48-
apache/django.wsgi
49-
5047
# VS Code related files
5148
.history/*
5249

environment.yml

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,11 @@
11
# First, install a conda environment with RMG-Py.
22
# Then, update the environment using the constraints in this environment file.
33

4-
name: rmg_env
4+
name: rmg_website
55
channels:
6-
- rmg
76
- conda-forge
8-
- cantera
9-
- fhvermei
107
dependencies:
11-
- docutils
12-
- django =2.2
13-
- lxml
14-
- pip
15-
- pip:
16-
- git+https://github.com/bp-kelley/descriptastorus@2.5.0
17-
- python >=3.7
18-
- solprop_ml >=1.2
8+
- django==4.2
9+
- python>=3.9
1910
- xlsxwriter
11+
- cantera==2.6.0*

microservices/solprop/Dockerfile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
FROM continuumio/miniconda3:latest
2+
3+
WORKDIR /app
4+
5+
RUN conda create -n solprop conda-forge::python=3.7 fhvermei::solprop_ml "conda-forge::scipy<1.11" && \
6+
conda install -n solprop rmg::descriptastorus && \
7+
conda install -n solprop conda-forge::fastapi conda-forge::uvicorn conda-forge::pydantic conda-forge::requests && \
8+
conda clean -a -y
9+
10+
COPY server.py .
11+
12+
EXPOSE 8081
13+
14+
CMD ["conda", "run", "--no-capture-output", "-n", "solprop", "uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8081"]

microservices/solprop/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Wraps the solprop package into a callable microservice.
2+
3+
With docker installed, run `docker build -t solprop_service .` in this directory to build the image.
4+
After, run `docker run -d -p 8081:8081 --name solprop solprop_service` to start the server.
5+
You can check the server outputs with `docker logs solprop`.
6+
7+
A prebuilt version of this image is also available on the ReactionMechanismGenerator DockerHub, which you can download and run to avoid building it yourself: https://hub.docker.com/layers/reactionmechanismgenerator/solprop_microservice/1.0.0/images/sha256-ca48dc8d0b0759a9a750c747758c5e095bf0dfcaae8fe0f64912ce8a91c764ff

microservices/solprop/server.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
from fastapi import FastAPI
2+
from pydantic import BaseModel
3+
import pandas as pd
4+
import numpy as np
5+
from typing import Optional
6+
7+
from chemprop_solvation.solvation_estimator import load_DirectML_Gsolv_estimator, load_DirectML_Hsolv_estimator, load_SoluteML_estimator
8+
from solvation_predictor.solubility.solubility_calculator import SolubilityCalculations
9+
from solvation_predictor.solubility.solubility_predictions import SolubilityPredictions
10+
from solvation_predictor.solubility.solubility_data import SolubilityData
11+
from solvation_predictor.solubility.solubility_models import SolubilityModels
12+
13+
14+
class SolubilityDataWrapper:
15+
"""
16+
Class for storing the input data for solubility prediction
17+
"""
18+
def __init__(self, solvent_smiles=None, solute_smiles=None, temp=None, ref_solub=None, ref_solv=None):
19+
self.smiles_pairs = [(solvent_smiles, solute_smiles)]
20+
self.temperatures = np.array([temp]) if temp is not None else None
21+
self.reference_solubility = np.array([ref_solub]) if ref_solub is not None else None
22+
self.reference_solvents = np.array([ref_solv]) if ref_solv is not None else None
23+
24+
25+
app = FastAPI()
26+
27+
dGsolv_estimator = None
28+
dHsolv_estimator = None
29+
SoluteML_estimator = None
30+
solub_models = None
31+
32+
@app.on_event("startup")
33+
def load_models():
34+
global dGsolv_estimator, dHsolv_estimator, SoluteML_estimator, solub_models
35+
36+
print("Loading models...")
37+
38+
dGsolv_estimator = load_DirectML_Gsolv_estimator()
39+
dHsolv_estimator = load_DirectML_Hsolv_estimator()
40+
41+
solub_models = SolubilityModels(
42+
load_g=True, load_h=True,
43+
reduced_number=False, load_saq=True,
44+
load_solute=True, logger=None, verbose=False
45+
)
46+
47+
SoluteML_estimator = load_SoluteML_estimator()
48+
49+
print("Models loaded.")
50+
51+
class SolubilityRequest(BaseModel):
52+
solvent_smiles: Optional[str] = None
53+
solute_smiles: Optional[str] = None
54+
temperature: Optional[float] = None
55+
reference_solvent: Optional[str] = None
56+
reference_solubility: Optional[float] = None
57+
hsub298: Optional[float] = None
58+
cp_gas_298: Optional[float] = None
59+
cp_solid_298: Optional[float] = None
60+
use_reference: bool = False
61+
62+
@app.post("/dGsolv_estimator")
63+
def _dGsolv_estimator(req: SolubilityRequest):
64+
result = dGsolv_estimator([[req.solvent_smiles, req.solute_smiles]])
65+
return {
66+
"avg_pred": result[0],
67+
"epi_unc": result[1],
68+
"valid_indices": result[2]
69+
}
70+
71+
@app.post("/dHsolv_estimator")
72+
def _dHsolv_estimator(req: SolubilityRequest):
73+
result = dHsolv_estimator([[req.solvent_smiles, req.solute_smiles]])
74+
return {
75+
"avg_pred": result[0],
76+
"epi_unc": result[1],
77+
"valid_indices": result[2]
78+
}
79+
80+
@app.post("/SoluteML_estimator")
81+
def _SoluteML_estimator(req: SolubilityRequest):
82+
result = SoluteML_estimator([[req.solute_smiles]])
83+
return {
84+
"avg_pred": result[0],
85+
"epi_unc": result[1],
86+
"valid_indices": result[2]
87+
}
88+
89+
@app.post("/calc_solubility_no_ref")
90+
def _calc_solubility_no_ref(req: SolubilityRequest):
91+
"""
92+
Calculate solubility with no reference solvent and reference solubility
93+
"""
94+
hsubl_298 = np.array([req.hsub298]) if req.hsub298 is not None else None
95+
Cp_solid = np.array([req.cp_solid_298]) if req.cp_solid_298 is not None else None
96+
Cp_gas = np.array([req.cp_gas_298]) if req.cp_gas_298 is not None else None
97+
98+
solub_data = SolubilityDataWrapper(solvent_smiles=req.solvent_smiles, solute_smiles=req.solute_smiles, temp=req.temperature)
99+
predictions = SolubilityPredictions(solub_data, solub_models, predict_aqueous=True,
100+
predict_reference_solvents=False, predict_t_dep=True,
101+
predict_solute_parameters=True, verbose=False)
102+
calculations = SolubilityCalculations(predictions, calculate_aqueous=True,
103+
calculate_reference_solvents=False, calculate_t_dep=True,
104+
calculate_t_dep_with_t_dep_hdiss=True, verbose=False,
105+
hsubl_298=hsubl_298, Cp_solid=Cp_solid, Cp_gas=Cp_gas)
106+
107+
return {
108+
"logsT_method1": calculations.logs_T_with_const_hdiss_from_aq[0],
109+
"logsT_method2": calculations.logs_T_with_T_dep_hdiss_from_aq[0],
110+
"gsolv_T": calculations.gsolv_T[0],
111+
"hsolv_T": calculations.hsolv_T[0],
112+
"ssolv_T": calculations.ssolv_T[0],
113+
"hsubl_298": calculations.hsubl_298[0],
114+
"Cp_gas": calculations.Cp_gas[0],
115+
"Cp_solid": calculations.Cp_solid[0],
116+
"logs_T_with_T_dep_hdiss_error_message": None if calculations.logs_T_with_T_dep_hdiss_error_message is None else calculations.logs_T_with_T_dep_hdiss_error_message[0],
117+
}
118+
119+
120+
@app.post("/calc_solubility_with_ref")
121+
def _calc_solubility_with_ref(req: SolubilityRequest):
122+
"""
123+
Calculate solubility with a reference solvent and reference solubility
124+
"""
125+
hsubl_298 = np.array([req.hsub298]) if req.hsub298 is not None else None
126+
Cp_solid = np.array([req.cp_solid_298]) if req.cp_solid_298 is not None else None
127+
Cp_gas = np.array([req.cp_gas_298]) if req.cp_gas_298 is not None else None
128+
129+
solub_data = SolubilityDataWrapper(solvent_smiles=req.solvent_smiles, solute_smiles=req.solute_smiles, temp=req.temperature,
130+
ref_solub=req.reference_solubility, ref_solv=req.reference_solvent)
131+
predictions = SolubilityPredictions(solub_data, solub_models, predict_aqueous=False,
132+
predict_reference_solvents=True, predict_t_dep=True,
133+
predict_solute_parameters=True, verbose=False)
134+
calculations = SolubilityCalculations(predictions, calculate_aqueous=False,
135+
calculate_reference_solvents=True, calculate_t_dep=True,
136+
calculate_t_dep_with_t_dep_hdiss=True, verbose=False,
137+
hsubl_298=hsubl_298, Cp_solid=Cp_solid, Cp_gas=Cp_gas)
138+
139+
return {
140+
"logsT_method1": calculations.logs_T_with_const_hdiss_from_ref[0],
141+
"logsT_method2": calculations.logs_T_with_T_dep_hdiss_from_ref[0],
142+
"gsolv_T": calculations.gsolv_T[0],
143+
"hsolv_T": calculations.hsolv_T[0],
144+
"ssolv_T": calculations.ssolv_T[0],
145+
"hsubl_298": calculations.hsubl_298[0],
146+
"Cp_gas": calculations.Cp_gas[0],
147+
"Cp_solid": calculations.Cp_solid[0],
148+
"logs_T_with_T_dep_hdiss_error_message": None if calculations.logs_T_with_T_dep_hdiss_error_message is None else calculations.logs_T_with_T_dep_hdiss_error_message[0],
149+
}

microservices/solprop/test.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import requests
2+
3+
# The base URL where your server is running.
4+
# Change this if running on a different port or host.
5+
BASE_URL = "http://localhost:8081"
6+
7+
# ---------------------------------------------------------
8+
# Test Data Constants
9+
# ---------------------------------------------------------
10+
TEST_SOLVENT = "CCO" # Ethanol
11+
TEST_SOLUTE = "CC(=O)O" # Acetic Acid
12+
13+
# ---------------------------------------------------------
14+
# Tests for DirectML and SoluteML Estimators
15+
# ---------------------------------------------------------
16+
17+
def test_dGsolv_estimator_success():
18+
payload = {
19+
"solvent_smiles": TEST_SOLVENT,
20+
"solute_smiles": TEST_SOLUTE
21+
}
22+
23+
response = requests.post(f"{BASE_URL}/dGsolv_estimator", json=payload)
24+
25+
# Verify the HTTP response
26+
assert response.status_code == 200
27+
28+
# Verify the JSON payload structure
29+
data = response.json()
30+
assert "avg_pred" in data
31+
assert "epi_unc" in data
32+
assert "valid_indices" in data
33+
34+
35+
def test_dHsolv_estimator_success():
36+
payload = {
37+
"solvent_smiles": TEST_SOLVENT,
38+
"solute_smiles": TEST_SOLUTE
39+
}
40+
41+
response = requests.post(f"{BASE_URL}/dHsolv_estimator", json=payload)
42+
43+
assert response.status_code == 200
44+
45+
data = response.json()
46+
assert "avg_pred" in data
47+
assert "epi_unc" in data
48+
assert "valid_indices" in data
49+
50+
51+
def test_SoluteML_estimator_success():
52+
payload = {
53+
"solute_smiles": TEST_SOLUTE
54+
}
55+
56+
response = requests.post(f"{BASE_URL}/SoluteML_estimator", json=payload)
57+
58+
assert response.status_code == 200
59+
60+
data = response.json()
61+
assert "avg_pred" in data
62+
assert "epi_unc" in data
63+
assert "valid_indices" in data
64+
65+
66+
def test_invalid_payload_fails():
67+
# Sending a bad type (like string instead of float for temperature)
68+
# should raise a 422 Unprocessable Entity from Pydantic
69+
payload = {
70+
"temperature": "not_a_number"
71+
}
72+
response = requests.post(f"{BASE_URL}/dGsolv_estimator", json=payload)
73+
assert response.status_code == 422
74+
75+
76+
# ---------------------------------------------------------
77+
# Tests for the Solubility Calculators
78+
# ---------------------------------------------------------
79+
80+
def test_calc_solubility_no_ref():
81+
payload = {
82+
"solvent_smiles": TEST_SOLVENT,
83+
"solute_smiles": TEST_SOLUTE,
84+
"temperature": 298.15
85+
}
86+
87+
response = requests.post(f"{BASE_URL}/calc_solubility_no_ref", json=payload)
88+
89+
assert response.status_code == 200
90+
91+
data = response.json()
92+
93+
# based on the previous version from rmg.mit.edu
94+
assert round(data["logsT_method1"], 3) == 1.146
95+
assert round(data["logsT_method2"], 3) == 1.146
96+
assert round(data["gsolv_T"], 2) == -7.12
97+
assert round(data["hsolv_T"], 2) == -12.08
98+
assert round(data["ssolv_T"] * 1000, 2) == -16.62
99+
assert round(data["hsubl_298"], 2) == 14.86
100+
assert round(data["Cp_gas"], 2) == 15.93
101+
assert round(data["Cp_solid"], 2) == 22.18
102+
103+
104+
def test_calc_solubility_with_ref():
105+
payload = {
106+
"solvent_smiles": TEST_SOLVENT,
107+
"solute_smiles": TEST_SOLUTE,
108+
"temperature": 298.15,
109+
"reference_solvent": "O", # Water
110+
"reference_solubility": -1.5
111+
}
112+
113+
response = requests.post(f"{BASE_URL}/calc_solubility_with_ref", json=payload)
114+
115+
assert response.status_code == 200
116+
117+
data = response.json()
118+
assert round(data["logsT_method1"], 3) == -1.223
119+
assert round(data["logsT_method2"], 3) == -1.223
120+
assert round(data["gsolv_T"], 2) == -7.12
121+
assert round(data["hsolv_T"], 2) == -12.08
122+
assert round(data["ssolv_T"] * 1000, 2) == -16.62
123+
assert round(data["hsubl_298"], 2) == 14.86
124+
assert round(data["Cp_gas"], 2) == 15.93
125+
assert round(data["Cp_solid"], 2) == 22.18
126+
127+
if __name__ == "__main__":
128+
test_dGsolv_estimator_success()
129+
test_dHsolv_estimator_success()
130+
test_SoluteML_estimator_success()
131+
test_invalid_payload_fails()
132+
test_calc_solubility_no_ref()
133+
test_calc_solubility_with_ref()
134+
print("All tests passed!")

0 commit comments

Comments
 (0)