From 027402a791b31ad293bbd24410ef5e5a1d5adc6e Mon Sep 17 00:00:00 2001 From: JonStargaryen Date: Thu, 19 Dec 2024 15:40:36 +0100 Subject: [PATCH 1/4] repr params --- molviewspec/app/api/examples.py | 15 ++++++ molviewspec/molviewspec/builder.py | 82 ++++++++++++++++++++++++++---- molviewspec/molviewspec/nodes.py | 25 ++++++++- 3 files changed, 111 insertions(+), 11 deletions(-) diff --git a/molviewspec/app/api/examples.py b/molviewspec/app/api/examples.py index dc7774c..411451a 100644 --- a/molviewspec/app/api/examples.py +++ b/molviewspec/app/api/examples.py @@ -728,6 +728,21 @@ async def refs_example() -> MVSResponse: return PlainTextResponse(builder.get_state()) +@router.get("/repr-params") +async def repr_params_example() -> MVSResponse: + """ + Individual representations (cartoon, ball-and-stick, surface) can be further customized. The corresponding builder + function exposes additional method arguments depending on the chosen representation type. + """ + builder = create_builder() + component = ( + builder.download(url=_url_for_mmcif("4hhb")).parse(format="mmcif").model_structure(ref="structure").component() + ) + component.representation(type="cartoon", size_factor=1.5, tubular_helices=True) + component.representation(type="surface", ignore_hydrogens=True).opacity(opacity=0.8) + return PlainTextResponse(builder.get_state()) + + @router.get("/primitives/cube") async def primitives_cube_example() -> MVSResponse: """ diff --git a/molviewspec/molviewspec/builder.py b/molviewspec/molviewspec/builder.py index 1fb7f80..eb47514 100644 --- a/molviewspec/molviewspec/builder.py +++ b/molviewspec/molviewspec/builder.py @@ -8,7 +8,7 @@ import math import os from abc import ABC, abstractmethod -from typing import Literal, Self, Sequence +from typing import Any, Literal, Self, Sequence, overload from pydantic import BaseModel, PrivateAttr @@ -46,7 +46,7 @@ PrimitivesFromUriParams, PrimitivesParams, RefT, - RepresentationParams, + RepresentationTypeParams, RepresentationTypeT, SchemaFormatT, SchemaT, @@ -78,17 +78,14 @@ class _BuilderProtocol(ABC): @property @abstractmethod - def _root(self) -> Root: - ... + def _root(self) -> Root: ... @property @abstractmethod - def _node(self) -> Node: - ... + def _node(self) -> Node: ... @abstractmethod - def _add_child(self, node: Node) -> None: - ... + def _add_child(self, node: Node) -> None: ... class _Base(BaseModel, _BuilderProtocol): @@ -661,17 +658,82 @@ class Component(_Base, _FocusMixin): Builder step with operations relevant for a particular component. """ + @overload + def representation( + self, + *, + type: Literal["cartoon"], + size_factor: float = None, + tubular_helices: bool = None, + custom: CustomT = None, + ref: RefT = None, + ) -> Representation: + """ + Add a cartoon representation for this component. + :param type: the type of this representation ('cartoon') + :param size_factor: adjust the scale of the visuals (relative to 1.0) + :param tubular_helices: simplify helices to tubes + :param custom: optional, custom data to attach to this node + :param ref: optional, reference that can be used to access this node + :return: a builder that handles operations at representation level + """ + ... + + @overload + def representation( + self, + *, + type: Literal["ball_and_stick"], + ignore_hydrogens: bool = None, + size_factor: float = None, + custom: CustomT = None, + ref: RefT = None, + ) -> Representation: + """ + Add a ball-and-stick representation for this component. + :param type: the type of this representation ('ball_and_stick') + :param ignore_hydrogens: draw hydrogen atoms? + :param size_factor: adjust the scale of the visuals (relative to 1.0) + :param custom: optional, custom data to attach to this node + :param ref: optional, reference that can be used to access this node + :return: a builder that handles operations at representation level + """ + ... + + @overload + def representation( + self, + *, + type: Literal["surface"], + ignore_hydrogens: bool = None, + size_factor: float = None, + custom: CustomT = None, + ref: RefT = None, + ) -> Representation: + """ + Add a surface representation for this component. + :param type: the type of this representation ('surface') + :param ignore_hydrogens: draw hydrogen atoms? + :param size_factor: adjust the scale of the visuals (relative to 1.0) + :param custom: optional, custom data to attach to this node + :param ref: optional, reference that can be used to access this node + :return: a builder that handles operations at representation level + """ + ... + def representation( - self, *, type: RepresentationTypeT = "cartoon", custom: CustomT = None, ref: RefT = None + self, *, type: RepresentationTypeT = "cartoon", custom: CustomT = None, ref: RefT = None, **kwargs: Any ) -> Representation: """ Add a representation for this component. :param type: the type of representation, defaults to 'cartoon' :param custom: optional, custom data to attach to this node :param ref: optional, reference that can be used to access this node + :param kwargs: optional, representation-specific params :return: a builder that handles operations at representation level """ - params = make_params(RepresentationParams, locals()) + params_class = RepresentationTypeParams.get(type) + params = make_params(params_class, locals(), **kwargs) node = Node(kind="representation", params=params) self._add_child(node) return Representation(node=node, root=self._root) diff --git a/molviewspec/molviewspec/nodes.py b/molviewspec/molviewspec/nodes.py index acfed0b..b9083d6 100644 --- a/molviewspec/molviewspec/nodes.py +++ b/molviewspec/molviewspec/nodes.py @@ -5,7 +5,7 @@ from __future__ import annotations from datetime import datetime, timezone -from typing import Any, Literal, Mapping, Optional, Tuple, TypeVar, Union +from typing import Any, Dict, Literal, Mapping, Optional, Tuple, Type, TypeVar, Union from uuid import uuid4 from pydantic import BaseModel, Field @@ -438,6 +438,29 @@ class RepresentationParams(BaseModel): type: RepresentationTypeT = Field(description="Representation type, i.e. cartoon, ball-and-stick, etc.") +class CartoonParams(RepresentationParams): + size_factor: Optional[float] = Field(description="Scales the corresponding visuals.") + tubular_helices: Optional[bool] = Field(description="Simplify corkscrew helices to tubes.") + # TODO support for variable size, e.g. based on b-factors? + + +class BallAndStickParams(RepresentationParams): + ignore_hydrogens: Optional[bool] = Field(descripton="Controls whether hydrogen atoms are drawn.") + size_factor: Optional[float] = Field(description="Scales the corresponding visuals.") + + +class SurfaceParams(RepresentationParams): + ignore_hydrogens: Optional[bool] = Field(descripton="Controls whether hydrogen atoms are drawn.") + size_factor: Optional[float] = Field(description="Scales the corresponding visuals.") + + +RepresentationTypeParams: Dict[RepresentationTypeT, Type[Union[CartoonParams, BallAndStickParams, SurfaceParams]]] = { + "cartoon": CartoonParams, + "ball_and_stick": BallAndStickParams, + "surface": SurfaceParams, +} + + SchemaT = Literal[ "whole_structure", "entity", From 9a46576d468cf166a921cb92437c55eed2817281 Mon Sep 17 00:00:00 2001 From: JonStargaryen Date: Fri, 20 Dec 2024 16:11:16 +0100 Subject: [PATCH 2/4] tweak repr param typing --- CHANGELOG.md | 1 + molviewspec/app/api/examples.py | 2 +- molviewspec/molviewspec/nodes.py | 11 +++++------ 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e914a86..d0d6a8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file, following t - Breaking: Renamed geometrical primitive `line` to `tube` - Breaking: Change multiple geometrical primitive parameters - Multi-state data model tweaks +- Support for representation-specific parameters (customize presentation visuals) ## [v1.1.0] - 2024-12-09 diff --git a/molviewspec/app/api/examples.py b/molviewspec/app/api/examples.py index 8d1f4ae..8401bb4 100644 --- a/molviewspec/app/api/examples.py +++ b/molviewspec/app/api/examples.py @@ -736,7 +736,7 @@ async def repr_params_example() -> MVSResponse: """ builder = create_builder() component = ( - builder.download(url=_url_for_mmcif("4hhb")).parse(format="mmcif").model_structure(ref="structure").component() + builder.download(url=_url_for_mmcif("1a23")).parse(format="mmcif").model_structure(ref="structure").component() ) component.representation(type="cartoon", size_factor=1.5, tubular_helices=True) component.representation(type="surface", ignore_hydrogens=True).opacity(opacity=0.8) diff --git a/molviewspec/molviewspec/nodes.py b/molviewspec/molviewspec/nodes.py index f0cadab..dc07f6c 100644 --- a/molviewspec/molviewspec/nodes.py +++ b/molviewspec/molviewspec/nodes.py @@ -5,7 +5,7 @@ from __future__ import annotations from datetime import datetime, timezone -from typing import Any, Dict, Literal, Mapping, Optional, Tuple, Type, TypeVar, Union +from typing import Any, Literal, Mapping, Optional, Tuple, TypeVar, Union from uuid import uuid4 from pydantic import BaseModel, Field @@ -439,26 +439,25 @@ class RepresentationParams(BaseModel): class CartoonParams(RepresentationParams): + type: Literal["cartoon"] = "cartoon" size_factor: Optional[float] = Field(description="Scales the corresponding visuals.") tubular_helices: Optional[bool] = Field(description="Simplify corkscrew helices to tubes.") # TODO support for variable size, e.g. based on b-factors? class BallAndStickParams(RepresentationParams): + type: Literal["ball_and_stick"] = "ball_and_stick" ignore_hydrogens: Optional[bool] = Field(descripton="Controls whether hydrogen atoms are drawn.") size_factor: Optional[float] = Field(description="Scales the corresponding visuals.") class SurfaceParams(RepresentationParams): + type: Literal["surface"] = "surface" ignore_hydrogens: Optional[bool] = Field(descripton="Controls whether hydrogen atoms are drawn.") size_factor: Optional[float] = Field(description="Scales the corresponding visuals.") -RepresentationTypeParams: Dict[RepresentationTypeT, Type[Union[CartoonParams, BallAndStickParams, SurfaceParams]]] = { - "cartoon": CartoonParams, - "ball_and_stick": BallAndStickParams, - "surface": SurfaceParams, -} +RepresentationTypeParams = {t.__fields__["type"].default: t for t in (CartoonParams, BallAndStickParams, SurfaceParams)} SchemaT = Literal[ From 82922377e293792ed92d62a70b23c6409e1c7d02 Mon Sep 17 00:00:00 2001 From: JonStargaryen Date: Fri, 20 Dec 2024 16:19:25 +0100 Subject: [PATCH 3/4] validate against non-model params --- molviewspec/molviewspec/utils.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/molviewspec/molviewspec/utils.py b/molviewspec/molviewspec/utils.py index f36bd6d..a7fe605 100644 --- a/molviewspec/molviewspec/utils.py +++ b/molviewspec/molviewspec/utils.py @@ -11,15 +11,18 @@ def make_params(params_type: Type[TParams], values=None, /, **more_values: objec if values is None: values = {} result = {} + consumed = set() # propagate custom properties if values: custom_values = values.get("custom") if custom_values is not None: result["custom"] = custom_values + consumed.add("custom") ref = values.get("ref") if ref is not None: result["ref"] = ref + consumed.add("ref") for field in params_type.__fields__.values(): # must use alias here to properly resolve goodies like `schema_` @@ -27,10 +30,17 @@ def make_params(params_type: Type[TParams], values=None, /, **more_values: objec if more_values.get(key) is not None: result[key] = more_values[key] + consumed.add(key) elif values.get(key) is not None: result[key] = values[key] + consumed.add(key) elif field.default is not None: # currently not used result[key] = field.default + consumed.add(key) + + non_model_keys = set(more_values.keys()) - consumed + if non_model_keys: + raise ValueError(f"Encountered unknown attribute on {params_type}: {non_model_keys}") return result # type: ignore From a9b471b5909d6792e79be6b41357d20cd29e00e7 Mon Sep 17 00:00:00 2001 From: JonStargaryen Date: Mon, 23 Dec 2024 14:07:17 +0100 Subject: [PATCH 4/4] restrict validation to `more_values` --- molviewspec/molviewspec/utils.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/molviewspec/molviewspec/utils.py b/molviewspec/molviewspec/utils.py index a7fe605..df7cc25 100644 --- a/molviewspec/molviewspec/utils.py +++ b/molviewspec/molviewspec/utils.py @@ -11,18 +11,16 @@ def make_params(params_type: Type[TParams], values=None, /, **more_values: objec if values is None: values = {} result = {} - consumed = set() + consumed_more_values = set() # propagate custom properties if values: custom_values = values.get("custom") if custom_values is not None: result["custom"] = custom_values - consumed.add("custom") ref = values.get("ref") if ref is not None: result["ref"] = ref - consumed.add("ref") for field in params_type.__fields__.values(): # must use alias here to properly resolve goodies like `schema_` @@ -30,15 +28,13 @@ def make_params(params_type: Type[TParams], values=None, /, **more_values: objec if more_values.get(key) is not None: result[key] = more_values[key] - consumed.add(key) + consumed_more_values.add(key) elif values.get(key) is not None: result[key] = values[key] - consumed.add(key) elif field.default is not None: # currently not used result[key] = field.default - consumed.add(key) - non_model_keys = set(more_values.keys()) - consumed + non_model_keys = set(more_values.keys()) - consumed_more_values if non_model_keys: raise ValueError(f"Encountered unknown attribute on {params_type}: {non_model_keys}")