Skip to content

Commit

Permalink
Add resource for basic and acid PKA calculations
Browse files Browse the repository at this point in the history
  • Loading branch information
khyurri committed Jul 25, 2022
1 parent e1fcc74 commit 9899037
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 276 deletions.
26 changes: 17 additions & 9 deletions api/http/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ repos:
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/psf/black
rev: 19.3b0

- repo: local
hooks:
- id: black
language_version: python3
name: black
entry: black
language: system
types: [ python ]

- repo: local
hooks:
Expand All @@ -22,15 +25,20 @@ repos:
args:
[
"-sn",
"--rcfile=api/http/pylintrc",
"indigo_service",
"tests"
"--rcfile=api/http/pylintrc"
]
language: system
types: [ python ]


- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v0.910"
- repo: local
hooks:
- id: mypy
name: mypy
language: system
entry: mypy
args:
[
"--config-file",
"api/http/mypy.ini"
]
types: [ python ]
9 changes: 5 additions & 4 deletions api/http/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@ COPY setup.py *manylinux1_x86_64*.whl ./
# Required for indigo-renderer
RUN apt-get update && \
apt-get upgrade -y && \
apt-get install -y --no-install-recommends libfreetype6-dev libfontconfig1-dev
apt-get install -y --no-install-recommends libfreetype6-dev libfontconfig1-dev gcc

# Install if exists
RUN if ls ./*manylinux1_x86_64*.whl 1> /dev/null 2>&1; then pip3 install ./*manylinux1_x86_64*.whl; fi

RUN pip3 install -r requirements.txt -r requirements_dev.txt && \
RUN pip install --upgrade pip &&\
pip3 install -r requirements.txt -r requirements_dev.txt && \
pylint -sn --rcfile=pylintrc indigo_service tests && \
mypy indigo_service && \
pip3 install -e . && \
Expand All @@ -50,7 +51,7 @@ FROM python:3.9-slim-buster

RUN apt-get update && \
apt-get upgrade -y && \
apt-get install -y --no-install-recommends libfreetype6-dev libfontconfig1-dev
apt-get install -y --no-install-recommends libfreetype6-dev libfontconfig1-dev gcc

RUN mkdir -p /opt/indigo
WORKDIR /opt/indigo
Expand All @@ -62,6 +63,6 @@ COPY setup.py ./
RUN apt-get autoremove -y && \
rm -rf /var/lib/apt/lists/*

RUN pip3 install .
RUN pip install --upgrade pip && pip3 install .
# OVERRIDE ON ORCHESTRATION LEVEL
CMD indigo_service
116 changes: 53 additions & 63 deletions api/http/indigo_service/indigo_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def compounds(
jsonapi.ValidationRequest,
jsonapi.CompoundConvertRequest,
jsonapi.RenderRequest,
jsonapi.PKARequest,
],
) -> List[Tuple[str, jsonapi.CompoundFormat]]:
return service.extract_pairs(request.data.attributes.compound)
Expand All @@ -81,7 +82,7 @@ def targets(


@app.get(f"{BASE_URL_INDIGO}/version", response_model=jsonapi.VersionResponse)
def indigo_version() -> jsonapi.VersionResponse:
async def indigo_version() -> jsonapi.VersionResponse:
return jsonapi.make_version_response(indigo().version())


Expand All @@ -90,7 +91,7 @@ def indigo_version() -> jsonapi.VersionResponse:
response_model=jsonapi.SimilaritiesResponse,
response_model_exclude_unset=True,
)
def similarities(
async def similarities(
request: jsonapi.SimilaritiesRequest,
) -> jsonapi.SimilaritiesResponse:

Expand Down Expand Up @@ -122,7 +123,7 @@ def similarities(
response_model=jsonapi.MatchResponse, # type: ignore
response_model_exclude_unset=True,
)
def exact_match(request: jsonapi.MatchRequest) -> jsonapi.MatchResponse:
async def exact_match(request: jsonapi.MatchRequest) -> jsonapi.MatchResponse:
compound, *_ = service.extract_compounds(source(request))
target_pairs = targets(request)
target_compounds = service.extract_compounds(target_pairs)
Expand All @@ -149,7 +150,7 @@ def exact_match(request: jsonapi.MatchRequest) -> jsonapi.MatchResponse:
response_model=jsonapi.CompoundResponse,
response_model_exclude_unset=True,
)
def convert(
async def convert(
request: jsonapi.CompoundConvertRequest,
) -> jsonapi.CompoundResponse:
compound, *_ = service.extract_compounds(
Expand All @@ -165,7 +166,9 @@ def convert(
response_model=jsonapi.ValidationResponse,
response_model_exclude_unset=True,
)
def validate(request: jsonapi.ValidationRequest) -> jsonapi.ValidationResponse:
async def validate(
request: jsonapi.ValidationRequest,
) -> jsonapi.ValidationResponse:
compound, *_ = service.extract_compounds(compounds(request))
validations = request.data.attributes.validations
results = {}
Expand All @@ -179,7 +182,7 @@ def validate(request: jsonapi.ValidationRequest) -> jsonapi.ValidationResponse:
response_model=jsonapi.DescriptorResponse,
response_model_exclude_unset=True,
)
def descriptors(
async def descriptors(
request: jsonapi.DescriptorRequest,
) -> jsonapi.DescriptorResponse:
compound, *_ = service.extract_compounds(compounds(request))
Expand All @@ -190,12 +193,54 @@ def descriptors(
return jsonapi.make_descriptor_response(results)


@app.post(f"{BASE_URL_INDIGO}/pka", response_model=jsonapi.PKAResponse)
async def pka(request: jsonapi.PKARequest) -> jsonapi.PKAResponse:
compound, *_ = service.extract_compounds(compounds(request))
if request.data.attributes.pka_model == jsonapi.PKAModel.ADVANCED:
indigo().setOption("pKa-model", jsonapi.PKAModel.ADVANCED.value)
pka_model_level = request.data.attributes.pka_model_level
pka_model_min_level = request.data.attributes.pka_model_min_level
pka_values = jsonapi.AtomToValueContainer()
pka_type = request.data.attributes.pka_type
pka_model_build = request.data.attributes.pka_model_build

if pka_model_build is not None:
service.build_pka_model(
pka_model_build.sdf,
pka_model_build.max_level,
pka_model_build.threshold,
)

for atom in compound.iterateAtoms():
if pka_type == jsonapi.PKAType.BASIC:
pka_values.mappings.append(
jsonapi.AtomToValueMapping(
index=atom.index(),
symbol=atom.symbol(),
value=compound.getBasicPkaValue(
atom, pka_model_level, pka_model_min_level
),
)
)
elif pka_type == jsonapi.PKAType.ACID:
pka_values.mappings.append(
jsonapi.AtomToValueMapping(
index=atom.index(),
symbol=atom.symbol(),
value=compound.getAcidPkaValue(
atom, pka_model_level, pka_model_min_level
),
)
)
return jsonapi.make_pka_response(pka_values)


@app.post(
f"{BASE_URL_INDIGO}/commonBits",
response_model=jsonapi.CommonBitsResponse,
response_model_exclude_unset=True,
)
def common_bits(
async def common_bits(
request: jsonapi.CommonBitsRequest,
) -> jsonapi.CommonBitsResponse:
compound, *_ = service.extract_compounds(source(request))
Expand All @@ -209,7 +254,7 @@ def common_bits(


@app.post(f"{BASE_URL_INDIGO}/render", response_model=jsonapi.RenderResponse)
def render(
async def render(
request: jsonapi.RenderRequest,
) -> jsonapi.RenderResponse:
compound, *_ = service.extract_compounds(compounds(request))
Expand Down Expand Up @@ -245,58 +290,3 @@ def run_debug() -> None:

if __name__ == "__main__":
run_debug()


# TODO: /indigo/render with alternative responses types
# @app.post(f"{BASE_URL_INDIGO}/render")
# def render(
# request: jsonapi.RenderRequest,
# ) -> Union[Response, FileResponse]:
# compound, *_ = service.extract_compounds(compounds(request))
# output_format = request.data.attributes.outputFormat
# indigo_renderer = IndigoRenderer(indigo())
# indigo().setOption(
# "render-output-format", jsonapi.rendering_formats.get(output_format)
# )
# options = request.data.attributes.options
# if options:
# for option, value in options.items():
# if option == "render-output-format":
# raise HTTPException(
# status_code=400, detail="Choose only one output format"
# )
# indigo().setOption(option, value)
# if output_format == "image/png":
# result = indigo_renderer.renderToBuffer(compound).tobytes()
# response = Response(
# result,
# headers={"Content-Type": "image/png"}
# )
# elif output_format == "image/png;base64":
# result = indigo_renderer.renderToBuffer(compound).tobytes()
# decoded_image = base64.b64encode(result).decode("utf-8")
# image_base64 = f"data:image/png;base64,{decoded_image}"
# response = Response(
# image_base64,
# headers={"Content-Type": "image/png"}
# )
# elif output_format == "image/svg+xml":
# result = indigo_renderer.renderToString(compound)
# response = Response(
# result,
# headers={"Content-Type": "image/svg+xml"}
# )
# elif output_format == "application/pdf":
# result = indigo_renderer.renderToBuffer(compound).tobytes()
# response = Response(
# result, headers={
# "Content-Type": "application/pdf",
# "Content-Disposition": "attachment; filename=mol.pdf"
# }
# )
# else:
# raise HTTPException(
# status_code=400,
# detail=f"Incorrect output format {output_format}"
# )
# return response
63 changes: 57 additions & 6 deletions api/http/indigo_service/jsonapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,8 +515,6 @@ class Descriptors(str, Enum):
MONOISOTOPIC_MASS = "monoisotopicMass"
MOST_ABUNDANT_MASS = "mostAbundantMass"
NAME = "name"
GET_ACID_PKA_VALUE = "acidPkaValue"
GET_BASIC_PKA_VALUE = "basicPkaValue"
GROSS_FORMULA = "grossFormula"


Expand Down Expand Up @@ -563,8 +561,6 @@ class DescriptorResultModel(BaseModel):
monoisotopicMass: Optional[str]
mostAbundantMass: Optional[str]
name: Optional[str]
getAcidPkaValue: Optional[str]
getBasicPkaValue: Optional[str]
grossFormula: Optional[str]


Expand All @@ -585,6 +581,62 @@ def make_descriptor_response(
]


# PKA models


class PKAType(str, Enum):
ACID = "acid"
BASIC = "basic"


class PKAModel(str, Enum):
SIMPLE = "simple"
ADVANCED = "advanced"


class PKAModelBuild(BaseModel):
sdf: str
max_level: int
threshold: float


class PKARequestModel(BaseModel):
compound: CompoundObject
pka_model_build: Optional[PKAModelBuild]
pka_model: PKAModel
pka_type: PKAType = PKAType.ACID
pka_model_level: int = 0
pka_model_min_level: int = 0


class PKARequestModelType(BaseModel):
__root__ = "pka"


class AtomToValueMapping(BaseModel):
index: int
symbol: str
value: float


class AtomToValueContainer(BaseModel):
mappings: list[AtomToValueMapping] = []


class PKAResultModelType(BaseModel):
__root__ = "pkaResult"


PKARequest = Request[PKARequestModelType, PKARequestModel]
PKAResponse = Response[PKAResultModelType, AtomToValueContainer]


def make_pka_response(mappings: AtomToValueContainer) -> PKAResponse:
return PKAResponse(
**{"data": {"type": "pkaResult", "attributes": mappings}}
)


# Render


Expand Down Expand Up @@ -618,8 +670,7 @@ class RenderResultModel(BaseModel):


def make_render_response(
raw_image: bytes,
output_format: str,
raw_image: bytes, output_format: str
) -> RenderResponse:
if output_format == "image/svg+xml":
str_image = raw_image.decode("utf-8")
Expand Down
19 changes: 18 additions & 1 deletion api/http/indigo_service/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,19 @@
# limitations under the License.
#

import logging
import tempfile
from pathlib import Path
from typing import List, Optional, Tuple, Union

from indigo import IndigoObject
from indigo import IndigoException, IndigoObject
from indigo.inchi import IndigoInchi

from indigo_service import jsonapi
from indigo_service.indigo_tools import indigo

logger = logging.getLogger(__name__)


def extract_compounds(
pairs: List[Tuple[str, jsonapi.CompoundFormat]],
Expand Down Expand Up @@ -142,3 +147,15 @@ def get_descriptor(
compound: IndigoObject, descriptor: jsonapi.Descriptors
) -> str:
return str(getattr(compound, descriptor.value)())


def build_pka_model(sdf: str, max_level: int, threshold: float) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
model_file = Path(tmp_dir) / "model.sdf"
with open(model_file, mode="w", encoding="utf-8") as sdf_file:
sdf_file.write(sdf)
try:
indigo().buildPkaModel(max_level, threshold, str(model_file))
except IndigoException as err:
logger.exception(err)
raise IndigoException("Unable to build pka model") from err
Loading

0 comments on commit 9899037

Please sign in to comment.