Skip to content
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
a0db786
greatly loosen environment restrictions
jonwzheng Jul 1, 2025
e8652bc
add back chemprop_solvation
jonwzheng Jul 3, 2025
cf3f29a
clarify solvation imports
jonwzheng Jul 3, 2025
2588ce4
replace deprecated ifequal javascript with if
jonwzheng Jul 3, 2025
8167ffc
fix django, solvation code
jonwzheng Jul 3, 2025
5405efc
update gitignore with removal of apache
jonwzheng Jul 3, 2025
befe8e2
fix faulty byte decode
jonwzheng Jul 3, 2025
34d0ca6
remove java from fluxdiagram
jonwzheng Jul 3, 2025
cfde279
update solubility code with workaround for argparse
jonwzheng Jul 3, 2025
58e831d
progress toward making solprop work
jonwzheng Jul 3, 2025
ff1aa1c
pin django to 4.2
jonwzheng Jul 3, 2025
2f2b1e4
remove load solubility code as no longer needed
jonwzheng Jul 3, 2025
ab9b72a
restore graph vis, ensuring conda env is in path
jonwzheng Jul 3, 2025
fab8941
make migrations
jonwzheng Jul 3, 2025
7a1e358
update django setting for django3
jonwzheng Jul 3, 2025
69f7108
update deprecated ajax code for Django 3.1
jonwzheng Jul 3, 2025
b333507
refactor views code
jonwzheng Jul 3, 2025
9cd4ad2
Update environment file
jonwzheng Oct 1, 2025
ca97824
update env file with wandb req for solprop
jonwzheng Oct 1, 2025
dacd415
use chemprop_solvation for SoluteML estimator and not solprop.
jonwzheng Oct 16, 2025
abb48d5
Update how solubility data is loaded
jonwzheng Oct 16, 2025
b93ba99
fix: solvent_smiles and solute_smiles names in solub data loading
jonwzheng Oct 16, 2025
7192942
Update solubility code to be compatible
jonwzheng Oct 17, 2025
42f4a38
Add tags suggesting users to use fastsolv
jonwzheng Oct 17, 2025
ee17be0
Pin scipy version and descriptastorus
jonwzheng Oct 21, 2025
97fc7ac
add note indicating these models were re-trained
jonwzheng Oct 23, 2025
063fbdf
WIP: replace SoluteML with solprop_ml_mix
jonwzheng Oct 23, 2025
146e81f
working code for single soluteML
jonwzheng Oct 30, 2025
b4abaa8
Refactor DirectML solute to work for batch inputs
jonwzheng Oct 30, 2025
ac46534
Fix typo in DirectML
jonwzheng Nov 12, 2025
964ac95
solprop_ml_mix revert
jonwzheng Mar 19, 2026
b3e55de
Use original version of solprop in a new microservice
jonwzheng Mar 19, 2026
92bfeae
undo example change
JacksonBurns Mar 20, 2026
c7fb579
Update SoluteML
jonwzheng Mar 23, 2026
11d3845
solve evolution (add missing link)
JacksonBurns Mar 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,6 @@ rmgweb/media/rmg/tools/*
.pydevproject
.settings/*

# WSGI
apache/django.wsgi

# VS Code related files
.history/*

Expand Down
16 changes: 4 additions & 12 deletions environment.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
# First, install a conda environment with RMG-Py.
# Then, update the environment using the constraints in this environment file.

name: rmg_env
name: rmg_website
channels:
- rmg
- conda-forge
- cantera
- fhvermei
dependencies:
- docutils
- django =2.2
- lxml
- pip
- pip:
- git+https://github.com/bp-kelley/descriptastorus@2.5.0
- python >=3.7
- solprop_ml >=1.2
- django==4.2
- python>=3.9
- xlsxwriter
- cantera==2.6.0*
14 changes: 14 additions & 0 deletions microservices/solprop/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM continuumio/miniconda3:latest

WORKDIR /app

RUN conda create -n solprop conda-forge::python=3.7 fhvermei::solprop_ml "conda-forge::scipy<1.11" && \
conda install -n solprop rmg::descriptastorus && \
conda install -n solprop conda-forge::fastapi conda-forge::uvicorn conda-forge::pydantic conda-forge::requests && \
conda clean -a -y

COPY server.py .

EXPOSE 8081

CMD ["conda", "run", "--no-capture-output", "-n", "solprop", "uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8081"]
7 changes: 7 additions & 0 deletions microservices/solprop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Wraps the solprop package into a callable microservice.

With docker installed, run `docker build -t solprop_service .` in this directory to build the image.
After, run `docker run -d -p 8081:8081 --name solprop solprop_service` to start the server.
You can check the server outputs with `docker logs solprop`.

A prebuilt version of this image is also available on the ReactionMechanismGenerator DockerHub.
149 changes: 149 additions & 0 deletions microservices/solprop/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
from fastapi import FastAPI
from pydantic import BaseModel
import pandas as pd
import numpy as np
from typing import Optional

from chemprop_solvation.solvation_estimator import load_DirectML_Gsolv_estimator, load_DirectML_Hsolv_estimator, load_SoluteML_estimator
from solvation_predictor.solubility.solubility_calculator import SolubilityCalculations
from solvation_predictor.solubility.solubility_predictions import SolubilityPredictions
from solvation_predictor.solubility.solubility_data import SolubilityData
from solvation_predictor.solubility.solubility_models import SolubilityModels


class SolubilityDataWrapper:
"""
Class for storing the input data for solubility prediction
"""
def __init__(self, solvent_smiles=None, solute_smiles=None, temp=None, ref_solub=None, ref_solv=None):
self.smiles_pairs = [(solvent_smiles, solute_smiles)]
self.temperatures = np.array([temp]) if temp is not None else None
self.reference_solubility = np.array([ref_solub]) if ref_solub is not None else None
self.reference_solvents = np.array([ref_solv]) if ref_solv is not None else None


app = FastAPI()

dGsolv_estimator = None
dHsolv_estimator = None
SoluteML_estimator = None
solub_models = None

@app.on_event("startup")
def load_models():
global dGsolv_estimator, dHsolv_estimator, SoluteML_estimator, solub_models

print("Loading models...")

dGsolv_estimator = load_DirectML_Gsolv_estimator()
dHsolv_estimator = load_DirectML_Hsolv_estimator()

solub_models = SolubilityModels(
load_g=True, load_h=True,
reduced_number=False, load_saq=True,
load_solute=True, logger=None, verbose=False
)

SoluteML_estimator = load_SoluteML_estimator()

print("Models loaded.")

class SolubilityRequest(BaseModel):
solvent_smiles: Optional[str] = None
solute_smiles: Optional[str] = None
temperature: Optional[float] = None
reference_solvent: Optional[str] = None
reference_solubility: Optional[float] = None
hsub298: Optional[float] = None
cp_gas_298: Optional[float] = None
cp_solid_298: Optional[float] = None
use_reference: bool = False

@app.post("/dGsolv_estimator")
def _dGsolv_estimator(req: SolubilityRequest):
result = dGsolv_estimator([[req.solvent_smiles, req.solute_smiles]])
return {
"avg_pred": result[0],
"epi_unc": result[1],
"valid_indices": result[2]
}

@app.post("/dHsolv_estimator")
def _dHsolv_estimator(req: SolubilityRequest):
result = dHsolv_estimator([[req.solvent_smiles, req.solute_smiles]])
return {
"avg_pred": result[0],
"epi_unc": result[1],
"valid_indices": result[2]
}

@app.post("/SoluteML_estimator")
def _SoluteML_estimator(req: SolubilityRequest):
result = SoluteML_estimator([[req.solute_smiles]])
return {
"avg_pred": result[0],
"epi_unc": result[1],
"valid_indices": result[2]
}

@app.post("/calc_solubility_no_ref")
def _calc_solubility_no_ref(req: SolubilityRequest):
"""
Calculate solubility with no reference solvent and reference solubility
"""
hsubl_298 = np.array([req.hsub298]) if req.hsub298 is not None else None
Cp_solid = np.array([req.cp_solid_298]) if req.cp_solid_298 is not None else None
Cp_gas = np.array([req.cp_gas_298]) if req.cp_gas_298 is not None else None

solub_data = SolubilityDataWrapper(solvent_smiles=req.solvent_smiles, solute_smiles=req.solute_smiles, temp=req.temperature)
predictions = SolubilityPredictions(solub_data, solub_models, predict_aqueous=True,
predict_reference_solvents=False, predict_t_dep=True,
predict_solute_parameters=True, verbose=False)
calculations = SolubilityCalculations(predictions, calculate_aqueous=True,
calculate_reference_solvents=False, calculate_t_dep=True,
calculate_t_dep_with_t_dep_hdiss=True, verbose=False,
hsubl_298=hsubl_298, Cp_solid=Cp_solid, Cp_gas=Cp_gas)

return {
"logsT_method1": calculations.logs_T_with_const_hdiss_from_aq[0],
"logsT_method2": calculations.logs_T_with_T_dep_hdiss_from_aq[0],
"gsolv_T": calculations.gsolv_T[0],
"hsolv_T": calculations.hsolv_T[0],
"ssolv_T": calculations.ssolv_T[0],
"hsubl_298": calculations.hsubl_298[0],
"Cp_gas": calculations.Cp_gas[0],
"Cp_solid": calculations.Cp_solid[0],
"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],
}


@app.post("/calc_solubility_with_ref")
def _calc_solubility_with_ref(req: SolubilityRequest):
"""
Calculate solubility with a reference solvent and reference solubility
"""
hsubl_298 = np.array([req.hsub298]) if req.hsub298 is not None else None
Cp_solid = np.array([req.cp_solid_298]) if req.cp_solid_298 is not None else None
Cp_gas = np.array([req.cp_gas_298]) if req.cp_gas_298 is not None else None

solub_data = SolubilityDataWrapper(solvent_smiles=req.solvent_smiles, solute_smiles=req.solute_smiles, temp=req.temperature,
ref_solub=req.reference_solubility, ref_solv=req.reference_solvent)
predictions = SolubilityPredictions(solub_data, solub_models, predict_aqueous=False,
predict_reference_solvents=True, predict_t_dep=True,
predict_solute_parameters=True, verbose=False)
calculations = SolubilityCalculations(predictions, calculate_aqueous=False,
calculate_reference_solvents=True, calculate_t_dep=True,
calculate_t_dep_with_t_dep_hdiss=True, verbose=False,
hsubl_298=hsubl_298, Cp_solid=Cp_solid, Cp_gas=Cp_gas)

return {
"logsT_method1": calculations.logs_T_with_const_hdiss_from_ref[0],
"logsT_method2": calculations.logs_T_with_T_dep_hdiss_from_ref[0],
"gsolv_T": calculations.gsolv_T[0],
"hsolv_T": calculations.hsolv_T[0],
"ssolv_T": calculations.ssolv_T[0],
"hsubl_298": calculations.hsubl_298[0],
"Cp_gas": calculations.Cp_gas[0],
"Cp_solid": calculations.Cp_solid[0],
"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],
}
134 changes: 134 additions & 0 deletions microservices/solprop/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import requests

# The base URL where your server is running.
# Change this if running on a different port or host.
BASE_URL = "http://localhost:8081"

# ---------------------------------------------------------
# Test Data Constants
# ---------------------------------------------------------
TEST_SOLVENT = "CCO" # Ethanol
TEST_SOLUTE = "CC(=O)O" # Acetic Acid

# ---------------------------------------------------------
# Tests for DirectML and SoluteML Estimators
# ---------------------------------------------------------

def test_dGsolv_estimator_success():
payload = {
"solvent_smiles": TEST_SOLVENT,
"solute_smiles": TEST_SOLUTE
}

response = requests.post(f"{BASE_URL}/dGsolv_estimator", json=payload)

# Verify the HTTP response
assert response.status_code == 200

# Verify the JSON payload structure
data = response.json()
assert "avg_pred" in data
assert "epi_unc" in data
assert "valid_indices" in data


def test_dHsolv_estimator_success():
payload = {
"solvent_smiles": TEST_SOLVENT,
"solute_smiles": TEST_SOLUTE
}

response = requests.post(f"{BASE_URL}/dHsolv_estimator", json=payload)

assert response.status_code == 200

data = response.json()
assert "avg_pred" in data
assert "epi_unc" in data
assert "valid_indices" in data


def test_SoluteML_estimator_success():
payload = {
"solute_smiles": TEST_SOLUTE
}

response = requests.post(f"{BASE_URL}/SoluteML_estimator", json=payload)

assert response.status_code == 200

data = response.json()
assert "avg_pred" in data
assert "epi_unc" in data
assert "valid_indices" in data


def test_invalid_payload_fails():
# Sending a bad type (like string instead of float for temperature)
# should raise a 422 Unprocessable Entity from Pydantic
payload = {
"temperature": "not_a_number"
}
response = requests.post(f"{BASE_URL}/dGsolv_estimator", json=payload)
assert response.status_code == 422


# ---------------------------------------------------------
# Tests for the Solubility Calculators
# ---------------------------------------------------------

def test_calc_solubility_no_ref():
payload = {
"solvent_smiles": TEST_SOLVENT,
"solute_smiles": TEST_SOLUTE,
"temperature": 298.15
}

response = requests.post(f"{BASE_URL}/calc_solubility_no_ref", json=payload)

assert response.status_code == 200

data = response.json()

# based on the previous version from rmg.mit.edu
assert round(data["logsT_method1"], 3) == 1.146
assert round(data["logsT_method2"], 3) == 1.146
assert round(data["gsolv_T"], 2) == -7.12
assert round(data["hsolv_T"], 2) == -12.08
assert round(data["ssolv_T"] * 1000, 2) == -16.62
assert round(data["hsubl_298"], 2) == 14.86
assert round(data["Cp_gas"], 2) == 15.93
assert round(data["Cp_solid"], 2) == 22.18


def test_calc_solubility_with_ref():
payload = {
"solvent_smiles": TEST_SOLVENT,
"solute_smiles": TEST_SOLUTE,
"temperature": 298.15,
"reference_solvent": "O", # Water
"reference_solubility": -1.5
}

response = requests.post(f"{BASE_URL}/calc_solubility_with_ref", json=payload)

assert response.status_code == 200

data = response.json()
assert round(data["logsT_method1"], 3) == -1.223
assert round(data["logsT_method2"], 3) == -1.223
assert round(data["gsolv_T"], 2) == -7.12
assert round(data["hsolv_T"], 2) == -12.08
assert round(data["ssolv_T"] * 1000, 2) == -16.62
assert round(data["hsubl_298"], 2) == 14.86
assert round(data["Cp_gas"], 2) == 15.93
assert round(data["Cp_solid"], 2) == 22.18

if __name__ == "__main__":
test_dGsolv_estimator_success()
test_dHsolv_estimator_success()
test_SoluteML_estimator_success()
test_invalid_payload_fails()
test_calc_solubility_no_ref()
test_calc_solubility_with_ref()
print("All tests passed!")
Loading