Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add resource for basic and acid PKA calculations #789

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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