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

Custom props #37

Merged
merged 2 commits into from
Oct 16, 2024
Merged
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
58 changes: 37 additions & 21 deletions molviewspec/app/api/examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ async def label_example(id: str = "1lap") -> MVSResponse:
(whole.representation().color(color="red", selector=ComponentExpression(label_asym_id="A", label_seq_id=120)))

# label the residues with custom text & focus it (i.e., position camera)
comp = structure.component(selector=residue).label(text="ALA 120 A: My Label").focus()
# leverage vendor-specific properties to request non-covalent interactions in Mol*
comp.additional_properties(
molstar_show_non_covalent_interactions=True, molstar_non_covalent_interactions_radius_ang=5.0
)
structure.component(
selector=residue,
custom={"molstar_show_non_covalent_interactions": True, "molstar_non_covalent_interactions_radius_ang": 5.0},
).label(text="ALA 120 A: My Label").focus()

# structure.label_from_source(schema="residue", category_name="my_custom_cif_category")

Expand Down Expand Up @@ -260,19 +260,31 @@ async def additional_properties_example() -> MVSResponse:
"""
builder = create_builder()
(
builder.download(url=_url_for_mmcif("1cbs"))
builder.download(url=_url_for_mmcif("4hhb"))
.parse(format="mmcif")
# each node provides this method, which allows storing custom data
.additional_properties(test="You can put whatever is needed here.", will_be_dropped=True)
.additional_properties(chainable="Totally!")
# properties can be removed by setting them to None
.additional_properties(will_be_dropped=None)
.model_structure()
.component()
.representation()
# you can nest properties as needed
.additional_properties(options={"provide_vendor_specific_props": True, "aim": "Customize representations."})
.color(color="blue")
.representation(type="cartoon", custom={"a": "hello"})
.color(selector="protein", color="#0000ff", custom={"b": "ciao"})
.color(selector="ligand", color="#ff0000")
.color(color="#555555", custom={"c": "salut"})
)
return PlainTextResponse(builder.get_state())


@router.get("/refs")
async def refs_example() -> MVSResponse:
"""
MolViewSpec allows assigning string references to nodes that allow referencing them
from various parts of the tree later, for example when building primitive shapes.
"""
builder = create_builder()
(
builder.download(url=_url_for_mmcif("4hhb"))
.parse(format="mmcif")
.model_structure(ref="structure")
.component()
.representation(type="cartoon")
)
return PlainTextResponse(builder.get_state())

Expand Down Expand Up @@ -1274,11 +1286,11 @@ async def portfolio_entity(entity_id: str = "1", assembly_id: str = "1") -> MVSR
highlight = ENTITY_COLORS_1HDA.get(entity_id, "black")
for type, entities in ENTITIES_1HDA.items():
for ent in entities:
(struct
.component(selector=ComponentExpression(label_entity_id=ent))
.representation(type="ball_and_stick" if type=="ligand" else "cartoon")
.color(color = highlight if ent==entity_id else BASE_COLOR)
.transparency(transparency = 0 if ent==entity_id else BASE_TRANSPARENCY)
(
struct.component(selector=ComponentExpression(label_entity_id=ent))
.representation(type="ball_and_stick" if type == "ligand" else "cartoon")
.color(color=highlight if ent == entity_id else BASE_COLOR)
.transparency(transparency=0 if ent == entity_id else BASE_TRANSPARENCY)
)
builder.camera(**CAMERA_FOR_1HDA)
return PlainTextResponse(builder.get_state())
Expand Down Expand Up @@ -1421,8 +1433,12 @@ async def portfolio_modres() -> MVSResponse:
builder = create_builder()
structure_url = _url_for_mmcif(ID)
struct = builder.download(url=structure_url).parse(format="mmcif").assembly_structure(assembly_id=ASSEMBLY)
struct.component(selector="polymer").representation(type="cartoon").color(color=BASE_COLOR).transparency(transparency=BASE_TRANSPARENCY)
struct.component(selector="ligand").representation(type="ball_and_stick").color(color=BASE_COLOR).transparency(transparency=BASE_TRANSPARENCY)
struct.component(selector="polymer").representation(type="cartoon").color(color=BASE_COLOR).transparency(
transparency=BASE_TRANSPARENCY
)
struct.component(selector="ligand").representation(type="ball_and_stick").color(color=BASE_COLOR).transparency(
transparency=BASE_TRANSPARENCY
)
struct.component(selector=ComponentExpression(label_asym_id="A", label_seq_id=54)).tooltip(
text="Modified residue SUI: (3-amino-2,5-dioxo-1-pyrrolidinyl)acetic acid"
).representation(type="ball_and_stick").color(color="#ED645A")
Expand Down
55 changes: 34 additions & 21 deletions molviewspec/molviewspec/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import math
import os
from typing import Self, Sequence
from typing import Sequence

from pydantic import BaseModel, PrivateAttr

Expand All @@ -23,6 +23,7 @@
ComponentFromUriParams,
ComponentInlineParams,
ComponentSelectorT,
CustomT,
DescriptionFormatT,
DownloadParams,
FocusInlineParams,
Expand All @@ -34,6 +35,7 @@
Node,
ParseFormatT,
ParseParams,
RefT,
RepresentationParams,
RepresentationTypeT,
SchemaFormatT,
Expand Down Expand Up @@ -80,23 +82,6 @@ def _add_child(self, node: Node) -> None:
self._node.children = []
self._node.children.append(node)

def additional_properties(self, **kwargs) -> Self:
"""
Adds provided key-value pairs as additional properties to this node.
key=None to remove a property.
"""
properties = self._node.additional_properties or {}

for k, v in kwargs.items():
if v is None:
# remove props with value of None
properties.pop(k, None)
else:
properties[k] = v

self._node.additional_properties = properties or None
return self


class Root(_Base):
"""
Expand Down Expand Up @@ -190,10 +175,11 @@ def canvas(self, *, background_color: ColorT | None = None) -> Root:
self._add_child(node)
return self

def download(self, *, url: str) -> Download:
def download(self, *, url: str, ref: RefT = None) -> Download:
"""
Add a new structure to the builder by downloading structure data from a URL.
:param url: source of structure data
:param ref: optional, reference that can be used to access this node
:return: a builder that handles operations on the downloaded resource
"""
params = make_params(DownloadParams, locals())
Expand All @@ -216,10 +202,11 @@ class Download(_Base):
Builder step with operations needed after downloading structure data.
"""

def parse(self, *, format: ParseFormatT) -> Parse:
def parse(self, *, format: ParseFormatT, ref: RefT = None) -> Parse:
"""
Parse the content by specifying the file format.
:param format: specify the format of your structure data
:param ref: optional, reference that can be used to access this node
:return: a builder that handles operations on the parsed content
"""
params = make_params(ParseParams, locals())
Expand All @@ -239,6 +226,7 @@ def model_structure(
model_index: int | None = None,
block_index: int | None = None,
block_header: str | None = None,
ref: RefT = None,
) -> Structure:
"""
Create a structure for the deposited coordinates.
Expand All @@ -259,6 +247,7 @@ def assembly_structure(
model_index: int | None = None,
block_index: int | None = None,
block_header: str | None = None,
ref: RefT = None,
) -> Structure:
"""
Create an assembly structure.
Expand All @@ -281,6 +270,7 @@ def symmetry_structure(
model_index: int | None = None,
block_index: int | None = None,
block_header: str | None = None,
ref: RefT = None,
) -> Structure:
"""
Create symmetry structure for a given range of Miller indices.
Expand All @@ -303,6 +293,7 @@ def symmetry_mates_structure(
model_index: int | None = None,
block_index: int | None = None,
block_header: str | None = None,
ref: RefT = None,
) -> Structure:
"""
Create structure of symmetry mates.
Expand All @@ -326,10 +317,14 @@ def component(
self,
*,
selector: ComponentSelectorT | ComponentExpression | list[ComponentExpression] = "all",
custom: CustomT = None,
ref: RefT = None,
) -> Component:
"""
Define a new component/selection for the given structure.
:param selector: a predefined component selector or one or more component selection expressions
: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 component level
"""
params = make_params(ComponentInlineParams, locals())
Expand All @@ -348,6 +343,8 @@ def component_from_uri(
block_index: int | None = None,
schema: SchemaT,
field_values: str | list[str] | None = None,
custom: CustomT = None,
ref: RefT = None,
) -> Component:
"""
Define a new component/selection for the given structure by fetching additional data from a resource.
Expand All @@ -359,6 +356,8 @@ def component_from_uri(
:param block_index: only applies when format is 'cif' or 'bcif'
:param schema: granularity/type of the selection
:param field_values: create the component from rows that have any of these values in the field specified by `field_name`. If not provided, create the component from all rows.
: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 component level
"""
if isinstance(field_values, str):
Expand All @@ -377,6 +376,8 @@ def component_from_source(
block_index: int | None = None,
schema: SchemaT,
field_values: str | list[str] | None = None,
custom: CustomT = None,
ref: RefT = None,
) -> Component:
"""
Define a new component/selection for the given structure by using categories from the source file.
Expand All @@ -386,6 +387,8 @@ def component_from_source(
:param block_index: only applies when format is 'cif' or 'bcif'
:param schema: granularity/type of the selection
:param field_values: create the component from rows that have any of these values in the field specified by `field_name`. If not provided, create the component from all rows.
: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 component level
"""
if isinstance(field_values, str):
Expand Down Expand Up @@ -500,11 +503,15 @@ def transform(
*,
rotation: Sequence[float] | None = None,
translation: Sequence[float] | None = None,
custom: CustomT = None,
ref: RefT = None,
) -> Structure:
"""
Transform a structure by applying a rotation matrix and/or translation vector.
:param rotation: 9d vector describing the rotation, in column major (j * 3 + i indexing) format, this is equivalent to Fortran-order in numpy, to be multiplied from the left
:param translation: 3d vector describing the translation
:param custom: optional, custom data to attach to this node
:param ref: optional, reference that can be used to access this node
:return: this builder
"""
if rotation is not None:
Expand Down Expand Up @@ -538,10 +545,14 @@ class Component(_Base):
Builder step with operations relevant for a particular component.
"""

def representation(self, *, type: RepresentationTypeT = "cartoon") -> Representation:
def representation(
self, *, type: RepresentationTypeT = "cartoon", custom: CustomT = None, ref: RefT = None
) -> 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
:return: a builder that handles operations at representation level
"""
params = make_params(RepresentationParams, locals())
Expand Down Expand Up @@ -649,11 +660,13 @@ def color(
*,
color: ColorT,
selector: ComponentSelectorT | ComponentExpression | list[ComponentExpression] = "all",
custom: CustomT = None,
) -> Representation:
"""
Customize the color of this representation.
:param color: color using SVG color names or RGB hex code
:param selector: optional selector, defaults to applying the color to the whole representation
:param custom: optional, custom data to attach to this node
:return: this builder
"""
params = make_params(ColorInlineParams, locals())
Expand Down
19 changes: 16 additions & 3 deletions molviewspec/molviewspec/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,30 @@
]


CustomT = Optional[Mapping[str, Any]]
RefT = Optional[str]


class Node(BaseModel):
"""
Base impl of all state tree nodes.
"""

kind: KindT = Field(description="The type of this node.")
params: Optional[Mapping[str, Any]] = Field(description="Optional params that are needed for this node.")
additional_properties: Optional[Mapping[str, Any]] = Field(
description="Optional free-style dict with custom, non-schema props."
)
children: Optional[list[Node]] = Field(description="Optional collection of nested child nodes.")
custom: CustomT = Field(description="Custom data to store attached to this node.")
ref: RefT = Field(description="Optional reference that can be used to access this node.")
Comment on lines +54 to +55
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be simpler to just include "custom" and "ref" as one of the node params? It would be defined in a params base class that all param classes would extend.

Instead of for example:

class ComponentInlineParams(BaseModel):
    selector: Union[ComponentSelectorT, ComponentExpression, list[ComponentExpression]] = Field(...)

We would have:

class BaseParams(BaseModel):
    custom: ...
    ref: ...
class ComponentInlineParams(BaseParams):
    selector: Union[ComponentSelectorT, ComponentExpression, list[ComponentExpression]] = Field(...)

I believe this will simplify code in Node.__init__ and make_params as they won't need to treat "custom" and "ref" in any special way.

Copy link
Member Author

@dsehnal dsehnal Oct 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be the benefit of this other than simpler params?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although, you are right, this would also allow to not have ref/custom on all nodes for example if we ever don't want to support it everywhere.


def __init__(self, **data):
# extract `custom` value from `params`
params = data.get("params", {})
if "custom" in params:
data["custom"] = params.pop("custom")
if "ref" in params:
data["ref"] = params.pop("ref")
Comment on lines +62 to +63
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JonStargaryen @midlik Added support for refs as well which will be required to make the primitives works when showing lines/etc between different structures.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you wanted to say "Please add" instead of "Added"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, partially added it myself :)


super().__init__(**data)


class FormatMetadata(BaseModel):
Expand Down
14 changes: 10 additions & 4 deletions molviewspec/molviewspec/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,19 @@ def make_params(params_type: Type[TParams], values=None, /, **more_values: objec
values = {}
result = {}

for field in params_type.__fields__.values():
# special `additional_properties` param that might be part of locals() too but should never be part of `params`
if field.alias == "additional_properties":
continue
# propagate custom properties
if values:
custom_values = values.get("custom")
if custom_values is not None:
result["custom"] = custom_values
ref = values.get("ref")
if ref is not None:
result["ref"] = ref

for field in params_type.__fields__.values():
# must use alias here to properly resolve goodies like `schema_`
key = field.alias

if more_values.get(key) is not None:
result[key] = more_values[key]
elif values.get(key) is not None:
Expand Down