Scene API
This section documents the components related to the FlatProt Scene, which acts as a container for projected visual elements before rendering.
Scene Concept
The "Scene" in FlatProt acts as a container that holds all the elements to be visualized after they have been projected into 2D space (plus depth information). It's the bridge between the processed structural data and the final rendering step (e.g., SVG generation).
Key concepts:
- Container: The
Scene
object holds a collection of SceneElement
objects (like helices, sheets, coils, annotations).
- Coordinate Space: Elements within the scene typically exist in a 2D coordinate system (X, Y) representing the canvas, but they retain a Z-coordinate representing their depth relative to the viewer.
- Resolution: It often works with a
CoordinateResolver
to map abstract residue identifiers (like ChainID:ResidueIndex
) to the actual 2D+Depth coordinates within the scene's context.
- Z-Ordering: The depth information (Z-coordinate) associated with elements allows renderers to draw them in the correct order, ensuring closer elements obscure farther ones. Annotations are typically given a very high depth value to ensure they are drawn on top.
- Rendering: The
Scene
object, along with its elements and their associated styles, provides all the necessary information for a Renderer
(like SVGRenderer
) to draw the final visualization.
Main Scene Class
The central container for all scene elements.
Manages the scene graph for a protein structure visualization.
The Scene holds the core protein structure data and a hierarchical
tree of SceneElements (nodes), including SceneGroups.
It provides methods to build and manipulate this tree, and to query
elements based on residue information.
Source code in src/flatprot/scene/scene.py
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464 | class Scene:
"""Manages the scene graph for a protein structure visualization.
The Scene holds the core protein structure data and a hierarchical
tree of SceneElements (nodes), including SceneGroups.
It provides methods to build and manipulate this tree, and to query
elements based on residue information.
"""
def __init__(self, structure: Structure):
"""Initializes the Scene with a core Structure object.
Args:
structure: The core biological structure data.
"""
if not isinstance(structure, Structure):
raise TypeError("Scene must be initialized with a Structure object.")
self._structure: Structure = structure
# List of top-level nodes (elements with no parent)
# The order here determines the base rendering order of top-level items.
self._nodes: List[BaseSceneElement] = []
# For quick lookups by ID
self._element_registry: Dict[str, BaseSceneElement] = {}
# Create the coordinate resolver instance
self._resolver: Optional[CoordinateResolver] = None # Initialize as None
@property
def structure(self) -> Structure:
"""Get the core Structure object associated with this scene."""
return self._structure
@property
def top_level_nodes(self) -> List[BaseSceneElement]:
"""Get the list of top-level nodes in the scene graph."""
return self._nodes
def get_element_by_id(self, id: str) -> Optional[BaseSceneElement]:
"""Retrieve a scene element by its unique ID.
Args:
id: The ID of the element to find.
Returns:
The found BaseSceneElement or None if no element has that ID.
"""
return self._element_registry.get(id)
def _register_element(self, element: BaseSceneElement) -> None:
"""Internal method to add an element to the ID registry."""
if element.id in self._element_registry:
# Raise specific error for duplicate IDs
raise DuplicateElementError(
f"Element with ID '{element.id}' already exists in the registry."
)
self._element_registry[element.id] = element
def _unregister_element(self, element: BaseSceneElement) -> None:
"""Internal method to remove an element from the ID registry."""
if element.id in self._element_registry:
del self._element_registry[element.id]
# Invalidate resolver cache if element affecting it is removed
self._resolver = None
def add_element(
self, element: BaseSceneElement, parent_id: Optional[str] = None
) -> None:
"""Adds a SceneElement to the scene graph.
If parent_id is provided, the element is added as a child of the
specified parent group. Otherwise, it's added as a top-level node.
Args:
element: The SceneElement to add.
parent_id: The ID of the parent SceneGroup, or None for top-level.
Raises:
ValueError: If parent_id is specified but not found, or if the
target parent is not a SceneGroup, or if the element ID
already exists.
TypeError: If the element is not a BaseSceneElement.
"""
if not isinstance(element, BaseSceneElement):
# Raise specific type error
raise ElementTypeError(
f"Object to add is not a BaseSceneElement subclass (got {type(element).__name__})."
)
# Check for existing element using ID registry (more reliable)
if element.id in self._element_registry:
raise DuplicateElementError(
f"Element with ID '{element.id}' already exists in the registry."
)
# Check if element object seems already attached (should have parent=None)
# This prevents adding the same *object* instance twice if somehow unregistered but still linked
if element.parent is not None:
raise InvalidSceneOperationError(
f"Element '{element.id}' already has a parent ('{element.parent.id}') and cannot be added directly."
)
# Check if it's already a top-level node (should not happen if parent is None check passes, but belt-and-suspenders)
if element in self._nodes:
raise InvalidSceneOperationError(
f"Element '{element.id}' is already a top-level node."
)
self._register_element(element) # Register first
parent: Optional[SceneGroup] = None
if parent_id is not None:
potential_parent = self.get_element_by_id(parent_id)
if potential_parent is None:
self._unregister_element(element) # Rollback
# Raise specific error for parent not found
raise ParentNotFoundError(
f"Parent group with ID '{parent_id}' not found."
)
if not isinstance(potential_parent, SceneGroup):
self._unregister_element(element) # Rollback
# Raise specific type error for parent
raise ElementTypeError(
f"Specified parent '{parent_id}' is not a SceneGroup (got {type(potential_parent).__name__})."
)
parent = potential_parent
try:
if parent:
# Let add_child raise its specific errors (e.g., ValueError for circular)
parent.add_child(element)
else:
element._set_parent(None)
self._nodes.append(element)
except (ValueError, TypeError, SceneError) as e:
# Catch potential errors from add_child or _set_parent
self._unregister_element(
element
) # Rollback registration (also invalidates resolver)
if parent is None and element in self._nodes:
self._nodes.remove(element) # Rollback adding to top-level
# Re-raise the original specific error, don't wrap
raise e
# Invalidate resolver cache if element affecting it is added
self._resolver = None
def remove_element(self, element_id: str) -> None:
"""Removes a SceneElement and its descendants from the scene graph by ID.
Args:
element_id: The ID of the SceneElement to remove.
Raises:
ValueError: If the element with the given ID is not found in the scene.
"""
element = self.get_element_by_id(element_id)
if element is None:
# Raise specific error
raise ElementNotFoundError(f"Element with ID '{element_id}' not found.")
# --- Collect nodes to remove --- (No change needed here)
nodes_to_unregister: List[BaseSceneElement] = []
nodes_to_process: List[BaseSceneElement] = [element]
while nodes_to_process:
node = nodes_to_process.pop(0)
# Check if already unregistered (in case of complex graph manipulations, though ideally not needed)
if node.id not in self._element_registry:
continue
nodes_to_unregister.append(node)
if isinstance(node, SceneGroup):
# Add children to process queue (create copy for safe iteration)
nodes_to_process.extend(list(node.children))
for node in nodes_to_unregister:
self._unregister_element(node)
# --- Detach the root element --- #
parent = element.parent
element_was_top_level = element in self._nodes
if parent:
if parent.id in self._element_registry and isinstance(parent, SceneGroup):
try:
parent.remove_child(element)
except ValueError:
# This *shouldn't* happen if graph is consistent. Treat as inconsistency.
# Log it, but also raise a specific error.
element._set_parent(None)
# Raise inconsistency error instead of just warning
raise SceneGraphInconsistencyError(
f"SceneGraph Inconsistency: Element '{element.id}' not found in supposed parent '{parent.id}' children list during removal."
)
else:
# Parent reference exists but parent is invalid/unregistered.
element._set_parent(None)
# This is also an inconsistency
raise SceneGraphInconsistencyError(
f"SceneGraph Inconsistency: Parent '{parent.id if parent else 'None'}' of element '{element.id}' is invalid or unregistered during removal."
)
elif element_was_top_level:
# If it was supposed to be top-level, remove it
self._nodes.remove(element)
element._set_parent(None)
else:
# Element was registered, had no parent, but wasn't in top-level nodes.
# This indicates an inconsistency.
element._set_parent(None)
raise SceneGraphInconsistencyError(
f"SceneGraph Inconsistency: Element '{element.id}' was registered but not found in the scene graph structure (neither parented nor top-level)."
)
# Invalidate resolver cache since elements were removed
self._resolver = None
def move_element(
self, element_id: str, new_parent_id: Optional[str] = None
) -> None:
"""Moves a SceneElement identified by its ID to a new parent.
Args:
element_id: The ID of the SceneElement to move.
new_parent_id: The ID of the new parent SceneGroup, or None to move
to the top level.
Raises:
ValueError: If the element or new parent is not found, if the new
parent is not a SceneGroup, or if the move would create
a circular dependency.
TypeError: If the target parent is not a SceneGroup.
"""
element = self.get_element_by_id(element_id)
if element is None:
raise ElementNotFoundError(f"Element with ID '{element_id}' not found.")
current_parent = element.parent
new_parent: Optional[SceneGroup] = None
if new_parent_id is not None:
potential_parent = self.get_element_by_id(new_parent_id)
if potential_parent is None:
raise ParentNotFoundError(
f"New parent group with ID '{new_parent_id}' not found."
)
if not isinstance(potential_parent, SceneGroup):
raise ElementTypeError(
f"Target parent '{new_parent_id}' is not a SceneGroup (got {type(potential_parent).__name__})."
)
new_parent = potential_parent
# Prevent circular dependency
temp_check: Optional[BaseSceneElement] = new_parent
while temp_check is not None:
if temp_check is element:
raise CircularDependencyError(
f"Cannot move element '{element_id}' under '{new_parent_id}' - would create circular dependency."
)
temp_check = temp_check.parent
if current_parent is new_parent:
return
# --- Detach Phase --- #
element_was_in_nodes = element in self._nodes
try:
if current_parent:
current_parent.remove_child(
element
) # Let SceneGroup handle internal parent update
elif element_was_in_nodes:
self._nodes.remove(element)
element._set_parent(None)
elif (
element.parent is None
): # Already detached, potentially inconsistent state
raise SceneGraphInconsistencyError(
f"SceneGraph Inconsistency: Element '{element_id}' was already detached before move operation."
)
else: # Should not be reachable if graph is consistent
raise SceneGraphInconsistencyError(
f"SceneGraph Inconsistency: Element '{element_id}' in inconsistent state during detach phase of move."
)
except ValueError as e:
# If remove_child fails unexpectedly (e.g., element not found when it should be)
element._set_parent(None) # Force detachment
raise SceneGraphInconsistencyError(
"Scene Graph Inconsistency: "
+ f"Error detaching '{element_id}' from current parent '{current_parent.id if current_parent else 'None'}': {e}"
) from e # Raise inconsistency
# --- Attach Phase --- #
try:
if new_parent:
new_parent.add_child(
element
) # Let add_child handle parent update & checks
else:
element._set_parent(None)
self._nodes.append(element)
except (ValueError, TypeError, SceneError) as e:
# If attaching fails, attempt rollback (reattach to original parent/location)
rollback_msg = f"Failed to attach element '{element_id}' to new parent '{new_parent_id}': {e}. Attempting rollback."
try:
if current_parent:
current_parent.add_child(
element
) # Try adding back to original parent
elif element_was_in_nodes: # If it was originally top-level
element._set_parent(None)
self._nodes.append(element)
# If originally detached, leave it detached
except Exception as rollback_e:
# Rollback failed, graph is likely inconsistent
msg = f"Rollback failed after attach error for element '{element.id}'. Scene graph may be inconsistent. Rollback error: {rollback_e}"
raise SceneGraphInconsistencyError(
msg
) from e # Raise inconsistency, chaining original error
raise InvalidSceneOperationError(rollback_msg) from e
# Invalidate resolver cache since element position changed
self._resolver = None
def traverse(self) -> Generator[Tuple[BaseSceneElement, int], None, None]:
"""Performs a depth-first traversal of the scene graph.
Yields:
Tuple[BaseSceneElement, int]: A tuple containing the scene element
and its depth in the tree (0 for top-level).
"""
nodes_to_visit: List[Tuple[BaseSceneElement, int]] = [
(node, 0) for node in reversed(self._nodes)
]
visited_ids: Set[str] = set()
while nodes_to_visit:
element, depth = nodes_to_visit.pop()
# Check registry in case node was removed during traversal (unlikely but possible)
if element.id not in self._element_registry or element.id in visited_ids:
continue
visited_ids.add(element.id)
yield element, depth
if isinstance(element, SceneGroup):
# Add children to the stack in reverse order to maintain visit order
# Ensure children are also still registered before adding
children_to_add = [
(child, depth + 1)
for child in reversed(element.children)
if child.id in self._element_registry
]
nodes_to_visit.extend(children_to_add)
def get_all_elements(self) -> List[BaseSceneElement]:
"""Returns a flat list of all elements in the scene graph.
Returns:
A list containing all BaseSceneElement objects registered in the scene.
"""
return list(self._element_registry.values())
def get_sequential_structure_elements(self) -> List[BaseStructureSceneElement]:
"""
Returns a list of all BaseStructureSceneElement instances in the scene,
sorted sequentially by chain ID and then by starting residue index.
Assumes each BaseStructureSceneElement primarily represents a single
contiguous range for sorting purposes.
Returns:
List[BaseStructureSceneElement]: Sorted list of structure elements.
"""
structure_elements: List[BaseStructureSceneElement] = []
for element in self._element_registry.values():
if isinstance(element, BaseStructureSceneElement):
structure_elements.append(element)
def sort_key(element: BaseStructureSceneElement) -> Tuple[str, int]:
primary_chain = "~" # Use ~ to sort after standard chain IDs
start_residue = float("inf") # Sort elements without range last
if element.residue_range_set and element.residue_range_set.ranges:
# Use the first range (min start residue) for sorting key
try:
first_range = min(
element.residue_range_set.ranges,
key=lambda r: (r.chain_id, r.start),
)
primary_chain = first_range.chain_id
start_residue = first_range.start
except (ValueError, TypeError) as e:
logger.warning(
f"Could not determine sort key for element {element.id} due to range issue: {e}"
)
else:
logger.debug(
f"Structure element {element.id} has no residue range for sorting."
)
# Return a tuple for multi-level sorting
return (primary_chain, start_residue)
try:
structure_elements.sort(key=sort_key)
except Exception as e:
logger.error(f"Error sorting structure elements: {e}", exc_info=True)
# Return unsorted list in case of unexpected sorting error
return structure_elements
def __iter__(self) -> Iterator[BaseSceneElement]:
"""Iterate over the top-level nodes of the scene graph."""
return iter(self._nodes)
def __len__(self) -> int:
"""Return the total number of elements currently registered in the scene."""
return len(self._element_registry)
def __repr__(self) -> str:
"""Provide a string representation of the scene."""
structure_id = getattr(self.structure, "id", "N/A") # Safely get structure ID
return (
f"<Scene structure_id='{structure_id}' "
f"top_level_nodes={len(self._nodes)} total_elements={len(self)}>"
)
# Lazily create resolver only when needed to avoid issues during Scene init
@property
def resolver(self) -> CoordinateResolver:
"""Get the CoordinateResolver instance for this scene."""
if self._resolver is None:
# Pass the current element registry
self._resolver = CoordinateResolver(self._structure, self._element_registry)
return self._resolver
|
resolver
property
Get the CoordinateResolver instance for this scene.
structure
property
Get the core Structure object associated with this scene.
top_level_nodes
property
Get the list of top-level nodes in the scene graph.
__init__(structure)
Initializes the Scene with a core Structure object.
Parameters: |
-
structure
(Structure )
–
The core biological structure data.
|
Source code in src/flatprot/scene/scene.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55 | def __init__(self, structure: Structure):
"""Initializes the Scene with a core Structure object.
Args:
structure: The core biological structure data.
"""
if not isinstance(structure, Structure):
raise TypeError("Scene must be initialized with a Structure object.")
self._structure: Structure = structure
# List of top-level nodes (elements with no parent)
# The order here determines the base rendering order of top-level items.
self._nodes: List[BaseSceneElement] = []
# For quick lookups by ID
self._element_registry: Dict[str, BaseSceneElement] = {}
# Create the coordinate resolver instance
self._resolver: Optional[CoordinateResolver] = None # Initialize as None
|
__iter__()
Iterate over the top-level nodes of the scene graph.
Source code in src/flatprot/scene/scene.py
| def __iter__(self) -> Iterator[BaseSceneElement]:
"""Iterate over the top-level nodes of the scene graph."""
return iter(self._nodes)
|
__len__()
Return the total number of elements currently registered in the scene.
Source code in src/flatprot/scene/scene.py
| def __len__(self) -> int:
"""Return the total number of elements currently registered in the scene."""
return len(self._element_registry)
|
__repr__()
Provide a string representation of the scene.
Source code in src/flatprot/scene/scene.py
449
450
451
452
453
454
455 | def __repr__(self) -> str:
"""Provide a string representation of the scene."""
structure_id = getattr(self.structure, "id", "N/A") # Safely get structure ID
return (
f"<Scene structure_id='{structure_id}' "
f"top_level_nodes={len(self._nodes)} total_elements={len(self)}>"
)
|
add_element(element, parent_id=None)
Adds a SceneElement to the scene graph.
If parent_id is provided, the element is added as a child of the
specified parent group. Otherwise, it's added as a top-level node.
Parameters: |
-
element
(BaseSceneElement )
–
-
parent_id
(Optional[str] , default:
None
)
–
The ID of the parent SceneGroup, or None for top-level.
|
Raises: |
-
ValueError
–
If parent_id is specified but not found, or if the
target parent is not a SceneGroup, or if the element ID
already exists.
-
TypeError
–
If the element is not a BaseSceneElement.
|
Source code in src/flatprot/scene/scene.py
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173 | def add_element(
self, element: BaseSceneElement, parent_id: Optional[str] = None
) -> None:
"""Adds a SceneElement to the scene graph.
If parent_id is provided, the element is added as a child of the
specified parent group. Otherwise, it's added as a top-level node.
Args:
element: The SceneElement to add.
parent_id: The ID of the parent SceneGroup, or None for top-level.
Raises:
ValueError: If parent_id is specified but not found, or if the
target parent is not a SceneGroup, or if the element ID
already exists.
TypeError: If the element is not a BaseSceneElement.
"""
if not isinstance(element, BaseSceneElement):
# Raise specific type error
raise ElementTypeError(
f"Object to add is not a BaseSceneElement subclass (got {type(element).__name__})."
)
# Check for existing element using ID registry (more reliable)
if element.id in self._element_registry:
raise DuplicateElementError(
f"Element with ID '{element.id}' already exists in the registry."
)
# Check if element object seems already attached (should have parent=None)
# This prevents adding the same *object* instance twice if somehow unregistered but still linked
if element.parent is not None:
raise InvalidSceneOperationError(
f"Element '{element.id}' already has a parent ('{element.parent.id}') and cannot be added directly."
)
# Check if it's already a top-level node (should not happen if parent is None check passes, but belt-and-suspenders)
if element in self._nodes:
raise InvalidSceneOperationError(
f"Element '{element.id}' is already a top-level node."
)
self._register_element(element) # Register first
parent: Optional[SceneGroup] = None
if parent_id is not None:
potential_parent = self.get_element_by_id(parent_id)
if potential_parent is None:
self._unregister_element(element) # Rollback
# Raise specific error for parent not found
raise ParentNotFoundError(
f"Parent group with ID '{parent_id}' not found."
)
if not isinstance(potential_parent, SceneGroup):
self._unregister_element(element) # Rollback
# Raise specific type error for parent
raise ElementTypeError(
f"Specified parent '{parent_id}' is not a SceneGroup (got {type(potential_parent).__name__})."
)
parent = potential_parent
try:
if parent:
# Let add_child raise its specific errors (e.g., ValueError for circular)
parent.add_child(element)
else:
element._set_parent(None)
self._nodes.append(element)
except (ValueError, TypeError, SceneError) as e:
# Catch potential errors from add_child or _set_parent
self._unregister_element(
element
) # Rollback registration (also invalidates resolver)
if parent is None and element in self._nodes:
self._nodes.remove(element) # Rollback adding to top-level
# Re-raise the original specific error, don't wrap
raise e
# Invalidate resolver cache if element affecting it is added
self._resolver = None
|
get_all_elements()
Returns a flat list of all elements in the scene graph.
Returns: |
-
List[BaseSceneElement]
–
A list containing all BaseSceneElement objects registered in the scene.
|
Source code in src/flatprot/scene/scene.py
384
385
386
387
388
389
390 | def get_all_elements(self) -> List[BaseSceneElement]:
"""Returns a flat list of all elements in the scene graph.
Returns:
A list containing all BaseSceneElement objects registered in the scene.
"""
return list(self._element_registry.values())
|
get_element_by_id(id)
Retrieve a scene element by its unique ID.
Parameters: |
-
id
(str )
–
The ID of the element to find.
|
Returns: |
-
Optional[BaseSceneElement]
–
The found BaseSceneElement or None if no element has that ID.
|
Source code in src/flatprot/scene/scene.py
67
68
69
70
71
72
73
74
75
76 | def get_element_by_id(self, id: str) -> Optional[BaseSceneElement]:
"""Retrieve a scene element by its unique ID.
Args:
id: The ID of the element to find.
Returns:
The found BaseSceneElement or None if no element has that ID.
"""
return self._element_registry.get(id)
|
get_sequential_structure_elements()
Returns a list of all BaseStructureSceneElement instances in the scene,
sorted sequentially by chain ID and then by starting residue index.
Assumes each BaseStructureSceneElement primarily represents a single
contiguous range for sorting purposes.
Source code in src/flatprot/scene/scene.py
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439 | def get_sequential_structure_elements(self) -> List[BaseStructureSceneElement]:
"""
Returns a list of all BaseStructureSceneElement instances in the scene,
sorted sequentially by chain ID and then by starting residue index.
Assumes each BaseStructureSceneElement primarily represents a single
contiguous range for sorting purposes.
Returns:
List[BaseStructureSceneElement]: Sorted list of structure elements.
"""
structure_elements: List[BaseStructureSceneElement] = []
for element in self._element_registry.values():
if isinstance(element, BaseStructureSceneElement):
structure_elements.append(element)
def sort_key(element: BaseStructureSceneElement) -> Tuple[str, int]:
primary_chain = "~" # Use ~ to sort after standard chain IDs
start_residue = float("inf") # Sort elements without range last
if element.residue_range_set and element.residue_range_set.ranges:
# Use the first range (min start residue) for sorting key
try:
first_range = min(
element.residue_range_set.ranges,
key=lambda r: (r.chain_id, r.start),
)
primary_chain = first_range.chain_id
start_residue = first_range.start
except (ValueError, TypeError) as e:
logger.warning(
f"Could not determine sort key for element {element.id} due to range issue: {e}"
)
else:
logger.debug(
f"Structure element {element.id} has no residue range for sorting."
)
# Return a tuple for multi-level sorting
return (primary_chain, start_residue)
try:
structure_elements.sort(key=sort_key)
except Exception as e:
logger.error(f"Error sorting structure elements: {e}", exc_info=True)
# Return unsorted list in case of unexpected sorting error
return structure_elements
|
move_element(element_id, new_parent_id=None)
Moves a SceneElement identified by its ID to a new parent.
Parameters: |
-
element_id
(str )
–
The ID of the SceneElement to move.
-
new_parent_id
(Optional[str] , default:
None
)
–
The ID of the new parent SceneGroup, or None to move
to the top level.
|
Raises: |
-
ValueError
–
If the element or new parent is not found, if the new
parent is not a SceneGroup, or if the move would create
a circular dependency.
-
TypeError
–
If the target parent is not a SceneGroup.
|
Source code in src/flatprot/scene/scene.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350 | def move_element(
self, element_id: str, new_parent_id: Optional[str] = None
) -> None:
"""Moves a SceneElement identified by its ID to a new parent.
Args:
element_id: The ID of the SceneElement to move.
new_parent_id: The ID of the new parent SceneGroup, or None to move
to the top level.
Raises:
ValueError: If the element or new parent is not found, if the new
parent is not a SceneGroup, or if the move would create
a circular dependency.
TypeError: If the target parent is not a SceneGroup.
"""
element = self.get_element_by_id(element_id)
if element is None:
raise ElementNotFoundError(f"Element with ID '{element_id}' not found.")
current_parent = element.parent
new_parent: Optional[SceneGroup] = None
if new_parent_id is not None:
potential_parent = self.get_element_by_id(new_parent_id)
if potential_parent is None:
raise ParentNotFoundError(
f"New parent group with ID '{new_parent_id}' not found."
)
if not isinstance(potential_parent, SceneGroup):
raise ElementTypeError(
f"Target parent '{new_parent_id}' is not a SceneGroup (got {type(potential_parent).__name__})."
)
new_parent = potential_parent
# Prevent circular dependency
temp_check: Optional[BaseSceneElement] = new_parent
while temp_check is not None:
if temp_check is element:
raise CircularDependencyError(
f"Cannot move element '{element_id}' under '{new_parent_id}' - would create circular dependency."
)
temp_check = temp_check.parent
if current_parent is new_parent:
return
# --- Detach Phase --- #
element_was_in_nodes = element in self._nodes
try:
if current_parent:
current_parent.remove_child(
element
) # Let SceneGroup handle internal parent update
elif element_was_in_nodes:
self._nodes.remove(element)
element._set_parent(None)
elif (
element.parent is None
): # Already detached, potentially inconsistent state
raise SceneGraphInconsistencyError(
f"SceneGraph Inconsistency: Element '{element_id}' was already detached before move operation."
)
else: # Should not be reachable if graph is consistent
raise SceneGraphInconsistencyError(
f"SceneGraph Inconsistency: Element '{element_id}' in inconsistent state during detach phase of move."
)
except ValueError as e:
# If remove_child fails unexpectedly (e.g., element not found when it should be)
element._set_parent(None) # Force detachment
raise SceneGraphInconsistencyError(
"Scene Graph Inconsistency: "
+ f"Error detaching '{element_id}' from current parent '{current_parent.id if current_parent else 'None'}': {e}"
) from e # Raise inconsistency
# --- Attach Phase --- #
try:
if new_parent:
new_parent.add_child(
element
) # Let add_child handle parent update & checks
else:
element._set_parent(None)
self._nodes.append(element)
except (ValueError, TypeError, SceneError) as e:
# If attaching fails, attempt rollback (reattach to original parent/location)
rollback_msg = f"Failed to attach element '{element_id}' to new parent '{new_parent_id}': {e}. Attempting rollback."
try:
if current_parent:
current_parent.add_child(
element
) # Try adding back to original parent
elif element_was_in_nodes: # If it was originally top-level
element._set_parent(None)
self._nodes.append(element)
# If originally detached, leave it detached
except Exception as rollback_e:
# Rollback failed, graph is likely inconsistent
msg = f"Rollback failed after attach error for element '{element.id}'. Scene graph may be inconsistent. Rollback error: {rollback_e}"
raise SceneGraphInconsistencyError(
msg
) from e # Raise inconsistency, chaining original error
raise InvalidSceneOperationError(rollback_msg) from e
# Invalidate resolver cache since element position changed
self._resolver = None
|
remove_element(element_id)
Removes a SceneElement and its descendants from the scene graph by ID.
Parameters: |
-
element_id
(str )
–
The ID of the SceneElement to remove.
|
Raises: |
-
ValueError
–
If the element with the given ID is not found in the scene.
|
Source code in src/flatprot/scene/scene.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243 | def remove_element(self, element_id: str) -> None:
"""Removes a SceneElement and its descendants from the scene graph by ID.
Args:
element_id: The ID of the SceneElement to remove.
Raises:
ValueError: If the element with the given ID is not found in the scene.
"""
element = self.get_element_by_id(element_id)
if element is None:
# Raise specific error
raise ElementNotFoundError(f"Element with ID '{element_id}' not found.")
# --- Collect nodes to remove --- (No change needed here)
nodes_to_unregister: List[BaseSceneElement] = []
nodes_to_process: List[BaseSceneElement] = [element]
while nodes_to_process:
node = nodes_to_process.pop(0)
# Check if already unregistered (in case of complex graph manipulations, though ideally not needed)
if node.id not in self._element_registry:
continue
nodes_to_unregister.append(node)
if isinstance(node, SceneGroup):
# Add children to process queue (create copy for safe iteration)
nodes_to_process.extend(list(node.children))
for node in nodes_to_unregister:
self._unregister_element(node)
# --- Detach the root element --- #
parent = element.parent
element_was_top_level = element in self._nodes
if parent:
if parent.id in self._element_registry and isinstance(parent, SceneGroup):
try:
parent.remove_child(element)
except ValueError:
# This *shouldn't* happen if graph is consistent. Treat as inconsistency.
# Log it, but also raise a specific error.
element._set_parent(None)
# Raise inconsistency error instead of just warning
raise SceneGraphInconsistencyError(
f"SceneGraph Inconsistency: Element '{element.id}' not found in supposed parent '{parent.id}' children list during removal."
)
else:
# Parent reference exists but parent is invalid/unregistered.
element._set_parent(None)
# This is also an inconsistency
raise SceneGraphInconsistencyError(
f"SceneGraph Inconsistency: Parent '{parent.id if parent else 'None'}' of element '{element.id}' is invalid or unregistered during removal."
)
elif element_was_top_level:
# If it was supposed to be top-level, remove it
self._nodes.remove(element)
element._set_parent(None)
else:
# Element was registered, had no parent, but wasn't in top-level nodes.
# This indicates an inconsistency.
element._set_parent(None)
raise SceneGraphInconsistencyError(
f"SceneGraph Inconsistency: Element '{element.id}' was registered but not found in the scene graph structure (neither parented nor top-level)."
)
# Invalidate resolver cache since elements were removed
self._resolver = None
|
traverse()
Performs a depth-first traversal of the scene graph.
Yields: |
-
Tuple[BaseSceneElement, int]
–
Tuple[BaseSceneElement, int]: A tuple containing the scene element
and its depth in the tree (0 for top-level).
|
Source code in src/flatprot/scene/scene.py
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382 | def traverse(self) -> Generator[Tuple[BaseSceneElement, int], None, None]:
"""Performs a depth-first traversal of the scene graph.
Yields:
Tuple[BaseSceneElement, int]: A tuple containing the scene element
and its depth in the tree (0 for top-level).
"""
nodes_to_visit: List[Tuple[BaseSceneElement, int]] = [
(node, 0) for node in reversed(self._nodes)
]
visited_ids: Set[str] = set()
while nodes_to_visit:
element, depth = nodes_to_visit.pop()
# Check registry in case node was removed during traversal (unlikely but possible)
if element.id not in self._element_registry or element.id in visited_ids:
continue
visited_ids.add(element.id)
yield element, depth
if isinstance(element, SceneGroup):
# Add children to the stack in reverse order to maintain visit order
# Ensure children are also still registered before adding
children_to_add = [
(child, depth + 1)
for child in reversed(element.children)
if child.id in self._element_registry
]
nodes_to_visit.extend(children_to_add)
|
options:
show_root_heading: true
members_order: source
Base Classes
Abstract base classes for elements and styles within the scene.
Bases: ABC
, Generic[StyleType]
Abstract base class for all elements within a scene graph.
This class is generic and requires a specific StyleType that inherits
from BaseSceneStyle.
Source code in src/flatprot/scene/base_element.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143 | class BaseSceneElement(ABC, Generic[StyleType]):
"""Abstract base class for all elements within a scene graph.
This class is generic and requires a specific StyleType that inherits
from BaseSceneStyle.
"""
def __init__(
self,
id: str,
style: Optional[StyleType] = None,
parent: Optional[SceneGroupType] = None,
):
"""Initializes a BaseSceneElement.
Args:
id: A unique identifier for this scene element.
style: A style instance for this element.
parent: The parent SceneGroup in the scene graph, if any.
"""
if not isinstance(id, str) or not id:
raise ValueError("SceneElement ID must be a non-empty string.")
self.id: str = id
self._style: Optional[StyleType] = style or self.default_style
self._parent: Optional[SceneGroupType] = parent
@property
def parent(self) -> Optional[SceneGroupType]:
"""Get the parent group of this element."""
return self._parent
# Keep internal setter for parent relationship management by Scene/SceneGroup
def _set_parent(self, value: Optional[SceneGroupType]) -> None:
"""Internal method to set the parent group. Should be called by Scene/SceneGroup."""
# Basic type check, assumes SceneGroup will inherit from BaseSceneElement
if value is not None and not isinstance(value, BaseSceneElement):
# A more specific check like isinstance(value, SceneGroup) would be ideal
# but causes circular dependency issues without careful structuring or protocols.
# This provides a basic safeguard. We expect SceneGroup to inherit BaseSceneElement.
raise TypeError(
"Parent must be a SceneGroup (subclass of BaseSceneElement)."
)
self._parent = value
@property
@abstractmethod
def default_style(self) -> StyleType:
"""Provides the default style instance for this element type.
Subclasses must implement this property.
Returns:
An instance of the specific StyleType for this element.
"""
raise NotImplementedError
@property
def style(self) -> StyleType:
"""Get the effective style for this element (instance-specific or default)."""
return self._style if self._style is not None else self.default_style()
def update_style(self, new_style: StyleType) -> None:
"""Update the instance-specific style of this element.
Args:
new_style: The new style object to apply.
"""
# Ensure the provided style is compatible
# Note: isinstance check might be too strict if subclasses of the style are allowed.
# Adjust check if necessary based on desired style inheritance behavior.
expected_style_type = self.default_style().__class__
if not isinstance(new_style, expected_style_type):
raise TypeError(
f"Invalid style type. Expected {expected_style_type.__name__}, "
f"got {type(new_style).__name__}."
)
self._style = new_style
@abstractmethod
def get_depth(self, structure: Structure) -> Optional[float]:
"""Calculate or retrieve the representative depth for Z-ordering.
Depth should typically be derived from the pre-projected coordinates
(column 2) in the provided structure object.
Lower values are typically closer to the viewer.
Args:
structure: The core Structure object containing pre-projected
2D + Depth coordinate data.
Returns:
A float representing the depth, or None if depth cannot be determined
or is not applicable (e.g., for groups).
"""
raise NotImplementedError
def __repr__(self) -> str:
"""Provide a string representation of the scene element."""
parent_id = f"'{self._parent.id}'" if self._parent else None
style_source = "default" if self._style is None else "instance"
# Safely get range representation, default to 'N/A' if not present
range_repr = str(getattr(self, "residue_range_set", "N/A"))
range_str = f" range='{range_repr}'" if range_repr != "N/A" else ""
target_repr = str(getattr(self, "target", "N/A"))
target_str = f" target='{target_repr}'" if target_repr != "N/A" else ""
return (
f"<{self.__class__.__name__} id='{self.id}'{range_str}{target_str} "
f"parent={parent_id} style_source={style_source}>"
)
|
default_style
abstractmethod
property
Provides the default style instance for this element type.
Subclasses must implement this property.
Returns: |
-
StyleType
–
An instance of the specific StyleType for this element.
|
parent
property
Get the parent group of this element.
style
property
Get the effective style for this element (instance-specific or default).
__init__(id, style=None, parent=None)
Initializes a BaseSceneElement.
Parameters: |
-
id
(str )
–
A unique identifier for this scene element.
-
style
(Optional[StyleType] , default:
None
)
–
A style instance for this element.
-
parent
(Optional[SceneGroupType] , default:
None
)
–
The parent SceneGroup in the scene graph, if any.
|
Source code in src/flatprot/scene/base_element.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58 | def __init__(
self,
id: str,
style: Optional[StyleType] = None,
parent: Optional[SceneGroupType] = None,
):
"""Initializes a BaseSceneElement.
Args:
id: A unique identifier for this scene element.
style: A style instance for this element.
parent: The parent SceneGroup in the scene graph, if any.
"""
if not isinstance(id, str) or not id:
raise ValueError("SceneElement ID must be a non-empty string.")
self.id: str = id
self._style: Optional[StyleType] = style or self.default_style
self._parent: Optional[SceneGroupType] = parent
|
__repr__()
Provide a string representation of the scene element.
Source code in src/flatprot/scene/base_element.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143 | def __repr__(self) -> str:
"""Provide a string representation of the scene element."""
parent_id = f"'{self._parent.id}'" if self._parent else None
style_source = "default" if self._style is None else "instance"
# Safely get range representation, default to 'N/A' if not present
range_repr = str(getattr(self, "residue_range_set", "N/A"))
range_str = f" range='{range_repr}'" if range_repr != "N/A" else ""
target_repr = str(getattr(self, "target", "N/A"))
target_str = f" target='{target_repr}'" if target_repr != "N/A" else ""
return (
f"<{self.__class__.__name__} id='{self.id}'{range_str}{target_str} "
f"parent={parent_id} style_source={style_source}>"
)
|
get_depth(structure)
abstractmethod
Calculate or retrieve the representative depth for Z-ordering.
Depth should typically be derived from the pre-projected coordinates
(column 2) in the provided structure object.
Lower values are typically closer to the viewer.
Parameters: |
-
structure
(Structure )
–
The core Structure object containing pre-projected
2D + Depth coordinate data.
|
Returns: |
-
Optional[float]
–
A float representing the depth, or None if depth cannot be determined
-
Optional[float]
–
or is not applicable (e.g., for groups).
|
Source code in src/flatprot/scene/base_element.py
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128 | @abstractmethod
def get_depth(self, structure: Structure) -> Optional[float]:
"""Calculate or retrieve the representative depth for Z-ordering.
Depth should typically be derived from the pre-projected coordinates
(column 2) in the provided structure object.
Lower values are typically closer to the viewer.
Args:
structure: The core Structure object containing pre-projected
2D + Depth coordinate data.
Returns:
A float representing the depth, or None if depth cannot be determined
or is not applicable (e.g., for groups).
"""
raise NotImplementedError
|
update_style(new_style)
Update the instance-specific style of this element.
Parameters: |
-
new_style
(StyleType )
–
The new style object to apply.
|
Source code in src/flatprot/scene/base_element.py
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110 | def update_style(self, new_style: StyleType) -> None:
"""Update the instance-specific style of this element.
Args:
new_style: The new style object to apply.
"""
# Ensure the provided style is compatible
# Note: isinstance check might be too strict if subclasses of the style are allowed.
# Adjust check if necessary based on desired style inheritance behavior.
expected_style_type = self.default_style().__class__
if not isinstance(new_style, expected_style_type):
raise TypeError(
f"Invalid style type. Expected {expected_style_type.__name__}, "
f"got {type(new_style).__name__}."
)
self._style = new_style
|
options:
show_root_heading: true
members_order: source
Bases: BaseModel
Base class for all scene element style definitions using Pydantic.
Source code in src/flatprot/scene/base_element.py
19
20
21
22
23
24
25
26
27
28
29
30 | class BaseSceneStyle(BaseModel):
"""Base class for all scene element style definitions using Pydantic."""
visibility: bool = Field(
default=True, description="Whether the element is visible."
)
opacity: float = Field(
default=1.0, ge=0.0, le=1.0, description="Opacity of the element (0.0 to 1.0)."
)
# Add other common style attributes here if needed later
model_config = {"extra": "forbid"} # Forbid extra fields
|
options:
show_root_heading: true
members_order: source
Structure Elements
Classes representing secondary structure elements within the scene.
Bases: BaseSceneElement[StructureStyleType]
, ABC
, Generic[StructureStyleType]
Abstract base class for scene elements representing structural components.
Automatically generates an ID based on the subclass type and residue range.
Requires a concrete style type inheriting from BaseStructureStyle.
Source code in src/flatprot/scene/structure/base_structure.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260 | class BaseStructureSceneElement(
BaseSceneElement[StructureStyleType], ABC, Generic[StructureStyleType]
):
"""Abstract base class for scene elements representing structural components.
Automatically generates an ID based on the subclass type and residue range.
Requires a concrete style type inheriting from BaseStructureStyle.
"""
def __init__(
self,
residue_range_set: ResidueRangeSet,
style: Optional[StructureStyleType] = None,
parent: Optional[SceneGroupType] = None,
):
"""Initializes a BaseStructureSceneElement.
The ID is generated automatically based on the concrete subclass name
and the provided residue range set.
Args:
residue_range_set: The set of residue ranges this element represents.
style: An optional specific style instance for this element. If None,
the default style defined by the subclass's `default_style`
property will be used at access time.
metadata: Optional dictionary for storing arbitrary metadata.
parent: The parent SceneGroup in the scene graph, if any.
"""
# Generate the ID *before* calling super().__init__
# Uses the concrete class's name (e.g., "Helix")
generated_id = self._generate_id(self.__class__, residue_range_set)
self.residue_range_set = residue_range_set
# Pass the potentially None style to the BaseSceneElement constructor.
# BaseSceneElement's `style` property handles returning the default
# style if the instance's `_style` attribute is None.
super().__init__(
id=generated_id,
style=style, # Pass the direct input style (can be None)
parent=parent,
)
@staticmethod
def _generate_id(cls: type, residue_range_set: ResidueRangeSet) -> str:
"""Generates a unique ID based on class name and residue range set."""
# Ensure the string representation of the range set is canonical and ID-friendly
# Replace spaces, commas, and colons to create a valid identifier part.
# Sorting ranges within the set ensures canonical representation if order matters.
# Assuming ResidueRangeSet.__str__ provides a consistent, sorted representation.
range_repr = (
str(residue_range_set).replace(" ", "").replace(",", "_").replace(":", "-")
)
return f"{cls.__name__}-{range_repr}"
@abstractmethod
def get_coordinates(self, structure: Structure) -> Optional[np.ndarray]:
"""Retrieve the final 2D + Depth coordinates for rendering this element.
Implementations should use the element's `residue_range_set` to query
the provided `structure` object (which is assumed to already contain
projected 2D + Depth coordinates) and return the relevant slice or
a simplified representation (e.g., lines for coils) based on these
pre-projected coordinates.
Args:
structure: The core Structure object containing pre-projected
2D + Depth coordinate data.
Returns:
A NumPy array of 2D + Depth coordinates (shape [N, 3] or similar)
suitable for rendering (X, Y, Depth).
"""
raise NotImplementedError
# Concrete subclasses (Helix, Sheet, etc.) MUST implement default_style
@property
@abstractmethod
def default_style(self) -> StructureStyleType:
"""Provides the default style instance for this specific element type.
Concrete subclasses (e.g., Helix, Sheet) must implement this property.
Returns:
An instance of the specific StyleType (e.g., HelixStyle) for this element.
"""
raise NotImplementedError
def get_coordinate_at_residue(
self, residue: ResidueCoordinate, structure: Structure
) -> Optional[np.ndarray]:
"""Retrieves the specific 2D canvas coordinate + Depth corresponding
to a given residue, derived from the pre-projected coordinates in the
structure object.
This default implementation assumes a direct mapping between the residue
index and the corresponding entry in the main structure.coordinates array.
Subclasses that implement complex coordinate calculations or simplifications
in their `get_coordinates` method (e.g., smoothing, interpolation)
MAY NEED TO OVERRIDE this method to provide the correct mapping to their
specific rendered representation.
Args:
residue: The residue coordinate (chain and index) to find the 2D point for.
structure: The core Structure object containing pre-projected 2D + Depth data.
Returns:
A NumPy array representing the calculated 2D coordinate + Depth (e.g., [X, Y, Depth]),
or None if the residue is not found or its coordinate cannot be determined.
"""
try:
# Check if the residue belongs to the range represented by this element
if residue not in self.residue_range_set:
logger.debug(
f"Residue {residue} not in range set {self.residue_range_set} for element '{self.id}'"
)
return None
chain = structure[residue.chain_id]
# Check if the residue index exists in this chain's mapping
if residue.residue_index not in chain:
logger.debug(
f"Residue {residue} not in chain {chain} for element '{self.id}'"
)
return None
coord_index = chain.coordinate_index(residue.residue_index)
if not (0 <= coord_index < len(structure.coordinates)):
struct_id = getattr(structure, "id", "N/A")
raise CoordinateCalculationError(
f"Coordinate index {coord_index} out of bounds for residue {residue} in structure '{struct_id}' (element '{self.id}')."
)
return structure.coordinates[coord_index]
except KeyError:
logger.debug(
f"Residue {residue} not in chain {chain} for element '{self.id}'"
)
return None
except (IndexError, AttributeError) as e:
struct_id = getattr(structure, "id", "N/A")
raise CoordinateCalculationError(
f"Error retrieving coordinate for {residue} in structure '{struct_id}' (element '{self.id}'): {e}"
) from e
def get_depth(self, structure: Structure) -> Optional[float]:
"""Calculate the mean depth of this structural element.
Calculates the mean of the depth values (column 2) of the
pre-projected coordinates corresponding to the residues in the
element's residue_range_set.
Args:
structure: The core Structure object containing pre-projected
2D + Depth coordinate data.
Returns:
The mean depth as a float, or None if no coordinates are found.
"""
# Get coordinates directly from the element's get_coordinates method
# which handles different element types appropriately
coords = self.get_coordinates(structure)
if coords is None or len(coords) == 0:
return None
# Extract depth values (Z-coordinate) from the coordinates
depths = coords[:, 2]
if len(depths) == 0:
return None
return float(np.mean(depths))
def is_adjacent_to(self, other: "BaseStructureSceneElement") -> bool:
"""Check if this element is adjacent to another element.
Args:
other: The other element to check adjacency with.
Returns:
True if the elements are adjacent, False otherwise.
"""
if not isinstance(other, BaseStructureSceneElement):
raise TypeError(f"Cannot check adjacency with {type(other)}")
return self.residue_range_set.is_adjacent_to(other.residue_range_set)
@abstractmethod
def get_start_connection_point(self, structure: Structure) -> Optional[np.ndarray]:
"""Get the 2D coordinate (X, Y) of the start connection point.
This is typically the coordinate corresponding to the first residue
in the element's range, projected onto the 2D canvas.
Args:
structure: The core Structure object with pre-projected coordinates.
Returns:
A NumPy array [X, Y] or None if not applicable/determinable.
"""
raise NotImplementedError
@abstractmethod
def get_end_connection_point(self, structure: Structure) -> Optional[np.ndarray]:
"""Get the 2D coordinate (X, Y) of the end connection point.
This is typically the coordinate corresponding to the last residue
in the element's range, projected onto the 2D canvas.
Args:
structure: The core Structure object with pre-projected coordinates.
Returns:
A NumPy array [X, Y] or None if not applicable/determinable.
"""
raise NotImplementedError
|
default_style
abstractmethod
property
Provides the default style instance for this specific element type.
Concrete subclasses (e.g., Helix, Sheet) must implement this property.
Returns: |
-
StructureStyleType
–
An instance of the specific StyleType (e.g., HelixStyle) for this element.
|
__init__(residue_range_set, style=None, parent=None)
Initializes a BaseStructureSceneElement.
The ID is generated automatically based on the concrete subclass name
and the provided residue range set.
Parameters: |
-
residue_range_set
(ResidueRangeSet )
–
The set of residue ranges this element represents.
-
style
(Optional[StructureStyleType] , default:
None
)
–
An optional specific style instance for this element. If None,
the default style defined by the subclass's default_style
property will be used at access time.
-
metadata
–
Optional dictionary for storing arbitrary metadata.
-
parent
(Optional[SceneGroupType] , default:
None
)
–
The parent SceneGroup in the scene graph, if any.
|
Source code in src/flatprot/scene/structure/base_structure.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85 | def __init__(
self,
residue_range_set: ResidueRangeSet,
style: Optional[StructureStyleType] = None,
parent: Optional[SceneGroupType] = None,
):
"""Initializes a BaseStructureSceneElement.
The ID is generated automatically based on the concrete subclass name
and the provided residue range set.
Args:
residue_range_set: The set of residue ranges this element represents.
style: An optional specific style instance for this element. If None,
the default style defined by the subclass's `default_style`
property will be used at access time.
metadata: Optional dictionary for storing arbitrary metadata.
parent: The parent SceneGroup in the scene graph, if any.
"""
# Generate the ID *before* calling super().__init__
# Uses the concrete class's name (e.g., "Helix")
generated_id = self._generate_id(self.__class__, residue_range_set)
self.residue_range_set = residue_range_set
# Pass the potentially None style to the BaseSceneElement constructor.
# BaseSceneElement's `style` property handles returning the default
# style if the instance's `_style` attribute is None.
super().__init__(
id=generated_id,
style=style, # Pass the direct input style (can be None)
parent=parent,
)
|
get_coordinate_at_residue(residue, structure)
Retrieves the specific 2D canvas coordinate + Depth corresponding
to a given residue, derived from the pre-projected coordinates in the
structure object.
This default implementation assumes a direct mapping between the residue
index and the corresponding entry in the main structure.coordinates array.
Subclasses that implement complex coordinate calculations or simplifications
in their get_coordinates
method (e.g., smoothing, interpolation)
MAY NEED TO OVERRIDE this method to provide the correct mapping to their
specific rendered representation.
Parameters: |
-
residue
(ResidueCoordinate )
–
The residue coordinate (chain and index) to find the 2D point for.
-
structure
(Structure )
–
The core Structure object containing pre-projected 2D + Depth data.
|
Returns: |
-
Optional[ndarray]
–
A NumPy array representing the calculated 2D coordinate + Depth (e.g., [X, Y, Depth]),
-
Optional[ndarray]
–
or None if the residue is not found or its coordinate cannot be determined.
|
Source code in src/flatprot/scene/structure/base_structure.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187 | def get_coordinate_at_residue(
self, residue: ResidueCoordinate, structure: Structure
) -> Optional[np.ndarray]:
"""Retrieves the specific 2D canvas coordinate + Depth corresponding
to a given residue, derived from the pre-projected coordinates in the
structure object.
This default implementation assumes a direct mapping between the residue
index and the corresponding entry in the main structure.coordinates array.
Subclasses that implement complex coordinate calculations or simplifications
in their `get_coordinates` method (e.g., smoothing, interpolation)
MAY NEED TO OVERRIDE this method to provide the correct mapping to their
specific rendered representation.
Args:
residue: The residue coordinate (chain and index) to find the 2D point for.
structure: The core Structure object containing pre-projected 2D + Depth data.
Returns:
A NumPy array representing the calculated 2D coordinate + Depth (e.g., [X, Y, Depth]),
or None if the residue is not found or its coordinate cannot be determined.
"""
try:
# Check if the residue belongs to the range represented by this element
if residue not in self.residue_range_set:
logger.debug(
f"Residue {residue} not in range set {self.residue_range_set} for element '{self.id}'"
)
return None
chain = structure[residue.chain_id]
# Check if the residue index exists in this chain's mapping
if residue.residue_index not in chain:
logger.debug(
f"Residue {residue} not in chain {chain} for element '{self.id}'"
)
return None
coord_index = chain.coordinate_index(residue.residue_index)
if not (0 <= coord_index < len(structure.coordinates)):
struct_id = getattr(structure, "id", "N/A")
raise CoordinateCalculationError(
f"Coordinate index {coord_index} out of bounds for residue {residue} in structure '{struct_id}' (element '{self.id}')."
)
return structure.coordinates[coord_index]
except KeyError:
logger.debug(
f"Residue {residue} not in chain {chain} for element '{self.id}'"
)
return None
except (IndexError, AttributeError) as e:
struct_id = getattr(structure, "id", "N/A")
raise CoordinateCalculationError(
f"Error retrieving coordinate for {residue} in structure '{struct_id}' (element '{self.id}'): {e}"
) from e
|
get_coordinates(structure)
abstractmethod
Retrieve the final 2D + Depth coordinates for rendering this element.
Implementations should use the element's residue_range_set
to query
the provided structure
object (which is assumed to already contain
projected 2D + Depth coordinates) and return the relevant slice or
a simplified representation (e.g., lines for coils) based on these
pre-projected coordinates.
Parameters: |
-
structure
(Structure )
–
The core Structure object containing pre-projected
2D + Depth coordinate data.
|
Returns: |
-
Optional[ndarray]
–
A NumPy array of 2D + Depth coordinates (shape [N, 3] or similar)
-
Optional[ndarray]
–
suitable for rendering (X, Y, Depth).
|
Source code in src/flatprot/scene/structure/base_structure.py
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117 | @abstractmethod
def get_coordinates(self, structure: Structure) -> Optional[np.ndarray]:
"""Retrieve the final 2D + Depth coordinates for rendering this element.
Implementations should use the element's `residue_range_set` to query
the provided `structure` object (which is assumed to already contain
projected 2D + Depth coordinates) and return the relevant slice or
a simplified representation (e.g., lines for coils) based on these
pre-projected coordinates.
Args:
structure: The core Structure object containing pre-projected
2D + Depth coordinate data.
Returns:
A NumPy array of 2D + Depth coordinates (shape [N, 3] or similar)
suitable for rendering (X, Y, Depth).
"""
raise NotImplementedError
|
get_depth(structure)
Calculate the mean depth of this structural element.
Calculates the mean of the depth values (column 2) of the
pre-projected coordinates corresponding to the residues in the
element's residue_range_set.
Parameters: |
-
structure
(Structure )
–
The core Structure object containing pre-projected
2D + Depth coordinate data.
|
Returns: |
-
Optional[float]
–
The mean depth as a float, or None if no coordinates are found.
|
Source code in src/flatprot/scene/structure/base_structure.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216 | def get_depth(self, structure: Structure) -> Optional[float]:
"""Calculate the mean depth of this structural element.
Calculates the mean of the depth values (column 2) of the
pre-projected coordinates corresponding to the residues in the
element's residue_range_set.
Args:
structure: The core Structure object containing pre-projected
2D + Depth coordinate data.
Returns:
The mean depth as a float, or None if no coordinates are found.
"""
# Get coordinates directly from the element's get_coordinates method
# which handles different element types appropriately
coords = self.get_coordinates(structure)
if coords is None or len(coords) == 0:
return None
# Extract depth values (Z-coordinate) from the coordinates
depths = coords[:, 2]
if len(depths) == 0:
return None
return float(np.mean(depths))
|
get_end_connection_point(structure)
abstractmethod
Get the 2D coordinate (X, Y) of the end connection point.
This is typically the coordinate corresponding to the last residue
in the element's range, projected onto the 2D canvas.
Parameters: |
-
structure
(Structure )
–
The core Structure object with pre-projected coordinates.
|
Returns: |
-
Optional[ndarray]
–
A NumPy array [X, Y] or None if not applicable/determinable.
|
Source code in src/flatprot/scene/structure/base_structure.py
247
248
249
250
251
252
253
254
255
256
257
258
259
260 | @abstractmethod
def get_end_connection_point(self, structure: Structure) -> Optional[np.ndarray]:
"""Get the 2D coordinate (X, Y) of the end connection point.
This is typically the coordinate corresponding to the last residue
in the element's range, projected onto the 2D canvas.
Args:
structure: The core Structure object with pre-projected coordinates.
Returns:
A NumPy array [X, Y] or None if not applicable/determinable.
"""
raise NotImplementedError
|
get_start_connection_point(structure)
abstractmethod
Get the 2D coordinate (X, Y) of the start connection point.
This is typically the coordinate corresponding to the first residue
in the element's range, projected onto the 2D canvas.
Parameters: |
-
structure
(Structure )
–
The core Structure object with pre-projected coordinates.
|
Returns: |
-
Optional[ndarray]
–
A NumPy array [X, Y] or None if not applicable/determinable.
|
Source code in src/flatprot/scene/structure/base_structure.py
232
233
234
235
236
237
238
239
240
241
242
243
244
245 | @abstractmethod
def get_start_connection_point(self, structure: Structure) -> Optional[np.ndarray]:
"""Get the 2D coordinate (X, Y) of the start connection point.
This is typically the coordinate corresponding to the first residue
in the element's range, projected onto the 2D canvas.
Args:
structure: The core Structure object with pre-projected coordinates.
Returns:
A NumPy array [X, Y] or None if not applicable/determinable.
"""
raise NotImplementedError
|
is_adjacent_to(other)
Check if this element is adjacent to another element.
Returns: |
-
bool
–
True if the elements are adjacent, False otherwise.
|
Source code in src/flatprot/scene/structure/base_structure.py
218
219
220
221
222
223
224
225
226
227
228
229
230 | def is_adjacent_to(self, other: "BaseStructureSceneElement") -> bool:
"""Check if this element is adjacent to another element.
Args:
other: The other element to check adjacency with.
Returns:
True if the elements are adjacent, False otherwise.
"""
if not isinstance(other, BaseStructureSceneElement):
raise TypeError(f"Cannot check adjacency with {type(other)}")
return self.residue_range_set.is_adjacent_to(other.residue_range_set)
|
options:
show_root_heading: true
Bases: BaseSceneStyle
Base style for elements representing parts of the protein structure.
Source code in src/flatprot/scene/structure/base_structure.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42 | class BaseStructureStyle(BaseSceneStyle):
"""Base style for elements representing parts of the protein structure."""
color: Color = Field(
default=Color((0.5, 0.5, 0.5)),
description="Default color for the element (hex string). Grey.",
)
stroke_color: Color = Field(
default=Color((0.0, 0.0, 0.0)),
description="Color for the stroke (hex string). Black.",
)
stroke_width: float = Field(
default=1.0, ge=0.0, description="Line width for stroke."
)
opacity: float = Field(
default=1.0, ge=0.0, le=1.0, description="Opacity for the element."
)
|
options:
show_root_heading: true
Bases: BaseStructureSceneElement[HelixStyle]
Represents an Alpha Helix segment, visualized as a zigzag ribbon.
Source code in src/flatprot/scene/structure/helix.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395 | class HelixSceneElement(BaseStructureSceneElement[HelixStyle]):
"""Represents an Alpha Helix segment, visualized as a zigzag ribbon."""
def __init__(
self,
residue_range_set: ResidueRangeSet,
style: Optional[HelixStyle] = None,
parent: Optional[SceneGroupType] = None,
):
"""Initializes the HelixSceneElement."""
super().__init__(residue_range_set, style, parent)
# Cache for the calculated zigzag coordinates and original length
self._cached_display_coords: Optional[np.ndarray] = None
self._original_coords_len: Optional[int] = None
@property
def default_style(self) -> HelixStyle:
"""Provides the default style for Helix elements."""
return HelixStyle()
def _get_original_coords_slice(self, structure: Structure) -> Optional[np.ndarray]:
"""Helper to extract the original coordinate slice for this helix."""
coords_list = []
if not self.residue_range_set.ranges:
# Cannot get coordinates if no ranges are defined.
raise CoordinateCalculationError(
f"Cannot get coordinates for Helix '{self.id}': no residue ranges defined."
)
helix_range = self.residue_range_set.ranges[0]
try:
chain = structure[helix_range.chain_id]
for res_idx in range(helix_range.start, helix_range.end + 1):
if res_idx in chain:
coord_idx = chain.coordinate_index(res_idx)
if 0 <= coord_idx < len(structure.coordinates):
coords_list.append(structure.coordinates[coord_idx])
else:
# Coordinate index out of bounds.
raise CoordinateCalculationError(
f"Coordinate index {coord_idx} out of bounds for residue {helix_range.chain_id}:{res_idx} in structure."
)
else:
# Residue not found in chain coordinate map.
raise CoordinateCalculationError(
f"Residue {helix_range.chain_id}:{res_idx} not found in chain coordinate map."
)
except (KeyError, IndexError, AttributeError) as e:
# Error fetching coordinates
raise CoordinateCalculationError(
f"Error fetching coordinates for helix '{self.id}': {e}"
) from e
return np.array(coords_list) if coords_list else None
def get_coordinates(self, structure: Structure) -> Optional[np.ndarray]:
"""Retrieve the 2D + Depth coordinates for the helix zigzag ribbon.
Calculates the ribbon shape based on start/end points of the pre-projected
coordinate slice and style parameters. Handles minimum length.
Args:
structure: The core Structure object containing pre-projected data.
Returns:
A NumPy array of the ribbon outline coordinates [X, Y, Depth],
or a simple line [start, end] if below min_helix_length.
"""
if self._cached_display_coords is not None:
return self._cached_display_coords
original_coords = self._get_original_coords_slice(structure)
if original_coords is None or len(original_coords) == 0:
self._cached_display_coords = None
self._original_coords_len = 0
return None
self._original_coords_len = len(original_coords)
if self._original_coords_len < 2:
# If only one residue, return just that point
self._cached_display_coords = np.array([original_coords[0]])
return self._cached_display_coords
# If too short, return a simple line (start and end points)
if self._original_coords_len < self.style.min_helix_length:
self._cached_display_coords = np.array(
[original_coords[0], original_coords[-1]]
)
return self._cached_display_coords
# Calculate zigzag points
start_point_3d = original_coords[0]
end_point_3d = original_coords[-1]
zigzag_coords = calculate_zigzag_points(
start_point_3d,
end_point_3d,
self.style.ribbon_thickness,
self.style.wavelength,
self.style.amplitude,
)
if (
zigzag_coords is None and self._original_coords_len >= 2
): # Log only if zigzag expected but failed
raise CoordinateCalculationError(
f"Could not generate zigzag points for helix '{self.id}' (length={self._original_coords_len}), likely zero length between endpoints."
)
self._cached_display_coords = zigzag_coords
return self._cached_display_coords
def get_coordinate_at_residue(
self, residue: ResidueCoordinate, structure: Structure
) -> Optional[np.ndarray]:
"""Retrieves the specific 2D coordinate + Depth corresponding to a residue
along the central axis of the helix representation.
For short helices, interpolates linearly. For zigzag helices, finds the
midpoint between the top and bottom ribbon points at the corresponding position.
Args:
residue: The residue coordinate (chain and index) to find the point for.
structure: The core Structure object containing pre-projected data.
Returns:
A NumPy array [X, Y, Depth] corresponding to the residue's position.
"""
# 1. Ensure display coordinates are calculated
display_coords = self.get_coordinates(structure)
if (
display_coords is None
or self._original_coords_len is None
or self._original_coords_len == 0
):
return None
# 2. Check if residue is within the element's range
if residue not in self.residue_range_set:
return None
element_range = self.residue_range_set.ranges[0] # Assuming single range
if residue.chain_id != element_range.chain_id:
return None
# 3. Calculate the 0-based index within the original sequence length
try:
original_sequence_index = residue.residue_index - element_range.start
if not (0 <= original_sequence_index < self._original_coords_len):
return None
except Exception:
return None
# Handle single point case
if self._original_coords_len == 1:
return display_coords[0] # Should be shape (1, 3)
# 4. Handle the case where a simple line was drawn
if len(display_coords) == 2:
# Linear interpolation along the line
frac = original_sequence_index / (self._original_coords_len - 1)
interpolated_coord = (
display_coords[0] * (1 - frac) + display_coords[1] * frac
)
return interpolated_coord
# 5. Handle the zigzag ribbon case
# display_coords contains top points then bottom points reversed
num_wave_points = len(display_coords) // 2 # Number of points along one edge
if num_wave_points < 1:
raise CoordinateCalculationError(
f"Invalid number of wave points ({num_wave_points}) for helix {self.id}"
)
# Map original sequence index to fractional position along the wave points (0 to num_wave_points-1)
mapped_wave_frac = (original_sequence_index * (num_wave_points - 1)) / (
self._original_coords_len - 1
)
# Find the indices in the display_coords array
idx_low = int(np.floor(mapped_wave_frac))
idx_high = min(idx_low + 1, num_wave_points - 1)
idx_low = min(idx_low, num_wave_points - 1) # Clamp low index too
frac = mapped_wave_frac - idx_low
# Get corresponding points on top and bottom edges
top_low = display_coords[idx_low]
top_high = display_coords[idx_high]
# Bottom indices are reversed: num_total - 1 - index
bottom_low = display_coords[len(display_coords) - 1 - idx_low]
bottom_high = display_coords[len(display_coords) - 1 - idx_high]
# Interpolate along top and bottom edges
interp_top = top_low * (1 - frac) + top_high * frac
interp_bottom = bottom_low * (1 - frac) + bottom_high * frac
# Return the midpoint between the interpolated top and bottom points
return (interp_top + interp_bottom) / 2.0
def get_start_connection_point(self, structure: Structure) -> Optional[np.ndarray]:
"""Calculate the 2D coordinate for the start connection point.
Args:
structure: The core Structure object containing projected coordinates.
Returns:
A NumPy array [X, Y] or None if calculation fails.
"""
# Get the full 3D coordinates used for rendering
display_coords = self.get_coordinates(structure)
if display_coords is None or len(display_coords) == 0:
return None
coords_2d = display_coords[:, :2] # Work with XY
# If rendered as a simple line (2 points)
if len(coords_2d) == 2:
return coords_2d[0]
# If rendered as zigzag (even number of points >= 4)
if len(coords_2d) >= 4:
# Midpoint of the starting edge
# First point (top edge start) = coords_2d[0]
# Corresponding bottom point (bottom edge start) = coords_2d[-1]
return (coords_2d[0] + coords_2d[-1]) / 2.0
# Fallback for unexpected cases (e.g., single point helix coord result)
return coords_2d[0] # Return the first point
def get_end_connection_point(self, structure: Structure) -> Optional[np.ndarray]:
"""Calculate the 2D coordinate for the end connection point.
Args:
structure: The core Structure object containing projected coordinates.
Returns:
A NumPy array [X, Y] or None if calculation fails.
"""
# Get the full 3D coordinates used for rendering
display_coords = self.get_coordinates(structure)
if display_coords is None or len(display_coords) == 0:
return None
coords_2d = display_coords[:, :2] # Work with XY
# If rendered as a simple line (2 points)
if len(coords_2d) == 2:
return coords_2d[1]
# If rendered as zigzag (even number of points >= 4)
if len(coords_2d) >= 4:
# Midpoint of the ending edge
# Last point of top edge = coords_2d[num_edge_points - 1]
# Corresponding last point of bottom edge = coords_2d[num_edge_points]
num_edge_points = len(coords_2d) // 2
last_top_point = coords_2d[num_edge_points - 1]
last_bottom_point = coords_2d[num_edge_points]
return (last_top_point + last_bottom_point) / 2.0
# Fallback for unexpected cases (e.g., single point helix coord result)
return coords_2d[-1] # Return the last point
|
default_style
property
Provides the default style for Helix elements.
__init__(residue_range_set, style=None, parent=None)
Initializes the HelixSceneElement.
Source code in src/flatprot/scene/structure/helix.py
137
138
139
140
141
142
143
144
145
146
147 | def __init__(
self,
residue_range_set: ResidueRangeSet,
style: Optional[HelixStyle] = None,
parent: Optional[SceneGroupType] = None,
):
"""Initializes the HelixSceneElement."""
super().__init__(residue_range_set, style, parent)
# Cache for the calculated zigzag coordinates and original length
self._cached_display_coords: Optional[np.ndarray] = None
self._original_coords_len: Optional[int] = None
|
get_coordinate_at_residue(residue, structure)
Retrieves the specific 2D coordinate + Depth corresponding to a residue
along the central axis of the helix representation.
For short helices, interpolates linearly. For zigzag helices, finds the
midpoint between the top and bottom ribbon points at the corresponding position.
Parameters: |
-
residue
(ResidueCoordinate )
–
The residue coordinate (chain and index) to find the point for.
-
structure
(Structure )
–
The core Structure object containing pre-projected data.
|
Returns: |
-
Optional[ndarray]
–
A NumPy array [X, Y, Depth] corresponding to the residue's position.
|
Source code in src/flatprot/scene/structure/helix.py
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332 | def get_coordinate_at_residue(
self, residue: ResidueCoordinate, structure: Structure
) -> Optional[np.ndarray]:
"""Retrieves the specific 2D coordinate + Depth corresponding to a residue
along the central axis of the helix representation.
For short helices, interpolates linearly. For zigzag helices, finds the
midpoint between the top and bottom ribbon points at the corresponding position.
Args:
residue: The residue coordinate (chain and index) to find the point for.
structure: The core Structure object containing pre-projected data.
Returns:
A NumPy array [X, Y, Depth] corresponding to the residue's position.
"""
# 1. Ensure display coordinates are calculated
display_coords = self.get_coordinates(structure)
if (
display_coords is None
or self._original_coords_len is None
or self._original_coords_len == 0
):
return None
# 2. Check if residue is within the element's range
if residue not in self.residue_range_set:
return None
element_range = self.residue_range_set.ranges[0] # Assuming single range
if residue.chain_id != element_range.chain_id:
return None
# 3. Calculate the 0-based index within the original sequence length
try:
original_sequence_index = residue.residue_index - element_range.start
if not (0 <= original_sequence_index < self._original_coords_len):
return None
except Exception:
return None
# Handle single point case
if self._original_coords_len == 1:
return display_coords[0] # Should be shape (1, 3)
# 4. Handle the case where a simple line was drawn
if len(display_coords) == 2:
# Linear interpolation along the line
frac = original_sequence_index / (self._original_coords_len - 1)
interpolated_coord = (
display_coords[0] * (1 - frac) + display_coords[1] * frac
)
return interpolated_coord
# 5. Handle the zigzag ribbon case
# display_coords contains top points then bottom points reversed
num_wave_points = len(display_coords) // 2 # Number of points along one edge
if num_wave_points < 1:
raise CoordinateCalculationError(
f"Invalid number of wave points ({num_wave_points}) for helix {self.id}"
)
# Map original sequence index to fractional position along the wave points (0 to num_wave_points-1)
mapped_wave_frac = (original_sequence_index * (num_wave_points - 1)) / (
self._original_coords_len - 1
)
# Find the indices in the display_coords array
idx_low = int(np.floor(mapped_wave_frac))
idx_high = min(idx_low + 1, num_wave_points - 1)
idx_low = min(idx_low, num_wave_points - 1) # Clamp low index too
frac = mapped_wave_frac - idx_low
# Get corresponding points on top and bottom edges
top_low = display_coords[idx_low]
top_high = display_coords[idx_high]
# Bottom indices are reversed: num_total - 1 - index
bottom_low = display_coords[len(display_coords) - 1 - idx_low]
bottom_high = display_coords[len(display_coords) - 1 - idx_high]
# Interpolate along top and bottom edges
interp_top = top_low * (1 - frac) + top_high * frac
interp_bottom = bottom_low * (1 - frac) + bottom_high * frac
# Return the midpoint between the interpolated top and bottom points
return (interp_top + interp_bottom) / 2.0
|
get_coordinates(structure)
Retrieve the 2D + Depth coordinates for the helix zigzag ribbon.
Calculates the ribbon shape based on start/end points of the pre-projected
coordinate slice and style parameters. Handles minimum length.
Parameters: |
-
structure
(Structure )
–
The core Structure object containing pre-projected data.
|
Returns: |
-
Optional[ndarray]
–
A NumPy array of the ribbon outline coordinates [X, Y, Depth],
-
Optional[ndarray]
–
or a simple line [start, end] if below min_helix_length.
|
Source code in src/flatprot/scene/structure/helix.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245 | def get_coordinates(self, structure: Structure) -> Optional[np.ndarray]:
"""Retrieve the 2D + Depth coordinates for the helix zigzag ribbon.
Calculates the ribbon shape based on start/end points of the pre-projected
coordinate slice and style parameters. Handles minimum length.
Args:
structure: The core Structure object containing pre-projected data.
Returns:
A NumPy array of the ribbon outline coordinates [X, Y, Depth],
or a simple line [start, end] if below min_helix_length.
"""
if self._cached_display_coords is not None:
return self._cached_display_coords
original_coords = self._get_original_coords_slice(structure)
if original_coords is None or len(original_coords) == 0:
self._cached_display_coords = None
self._original_coords_len = 0
return None
self._original_coords_len = len(original_coords)
if self._original_coords_len < 2:
# If only one residue, return just that point
self._cached_display_coords = np.array([original_coords[0]])
return self._cached_display_coords
# If too short, return a simple line (start and end points)
if self._original_coords_len < self.style.min_helix_length:
self._cached_display_coords = np.array(
[original_coords[0], original_coords[-1]]
)
return self._cached_display_coords
# Calculate zigzag points
start_point_3d = original_coords[0]
end_point_3d = original_coords[-1]
zigzag_coords = calculate_zigzag_points(
start_point_3d,
end_point_3d,
self.style.ribbon_thickness,
self.style.wavelength,
self.style.amplitude,
)
if (
zigzag_coords is None and self._original_coords_len >= 2
): # Log only if zigzag expected but failed
raise CoordinateCalculationError(
f"Could not generate zigzag points for helix '{self.id}' (length={self._original_coords_len}), likely zero length between endpoints."
)
self._cached_display_coords = zigzag_coords
return self._cached_display_coords
|
get_end_connection_point(structure)
Calculate the 2D coordinate for the end connection point.
Parameters: |
-
structure
(Structure )
–
The core Structure object containing projected coordinates.
|
Returns: |
-
Optional[ndarray]
–
A NumPy array [X, Y] or None if calculation fails.
|
Source code in src/flatprot/scene/structure/helix.py
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395 | def get_end_connection_point(self, structure: Structure) -> Optional[np.ndarray]:
"""Calculate the 2D coordinate for the end connection point.
Args:
structure: The core Structure object containing projected coordinates.
Returns:
A NumPy array [X, Y] or None if calculation fails.
"""
# Get the full 3D coordinates used for rendering
display_coords = self.get_coordinates(structure)
if display_coords is None or len(display_coords) == 0:
return None
coords_2d = display_coords[:, :2] # Work with XY
# If rendered as a simple line (2 points)
if len(coords_2d) == 2:
return coords_2d[1]
# If rendered as zigzag (even number of points >= 4)
if len(coords_2d) >= 4:
# Midpoint of the ending edge
# Last point of top edge = coords_2d[num_edge_points - 1]
# Corresponding last point of bottom edge = coords_2d[num_edge_points]
num_edge_points = len(coords_2d) // 2
last_top_point = coords_2d[num_edge_points - 1]
last_bottom_point = coords_2d[num_edge_points]
return (last_top_point + last_bottom_point) / 2.0
# Fallback for unexpected cases (e.g., single point helix coord result)
return coords_2d[-1] # Return the last point
|
get_start_connection_point(structure)
Calculate the 2D coordinate for the start connection point.
Parameters: |
-
structure
(Structure )
–
The core Structure object containing projected coordinates.
|
Returns: |
-
Optional[ndarray]
–
A NumPy array [X, Y] or None if calculation fails.
|
Source code in src/flatprot/scene/structure/helix.py
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362 | def get_start_connection_point(self, structure: Structure) -> Optional[np.ndarray]:
"""Calculate the 2D coordinate for the start connection point.
Args:
structure: The core Structure object containing projected coordinates.
Returns:
A NumPy array [X, Y] or None if calculation fails.
"""
# Get the full 3D coordinates used for rendering
display_coords = self.get_coordinates(structure)
if display_coords is None or len(display_coords) == 0:
return None
coords_2d = display_coords[:, :2] # Work with XY
# If rendered as a simple line (2 points)
if len(coords_2d) == 2:
return coords_2d[0]
# If rendered as zigzag (even number of points >= 4)
if len(coords_2d) >= 4:
# Midpoint of the starting edge
# First point (top edge start) = coords_2d[0]
# Corresponding bottom point (bottom edge start) = coords_2d[-1]
return (coords_2d[0] + coords_2d[-1]) / 2.0
# Fallback for unexpected cases (e.g., single point helix coord result)
return coords_2d[0] # Return the first point
|
options:
show_root_heading: true
Bases: BaseStructureStyle
Style properties specific to Helix elements.
Defines properties for rendering helices as zigzag ribbons.
Source code in src/flatprot/scene/structure/helix.py
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130 | class HelixStyle(BaseStructureStyle):
"""Style properties specific to Helix elements.
Defines properties for rendering helices as zigzag ribbons.
"""
# Override inherited defaults
color: Color = Field(
default=Color("#ff0000"), description="Default color for helix (red)."
)
stroke_width: float = Field(
default=1, description="Reference width for calculating helix dimensions."
)
simplified_width: float = Field(
default=2,
description="Width to use for simplified helix rendering (line only).",
)
# Helix-specific attributes
ribbon_thickness: float = Field(
default=8,
description="Factor to multiply linewidth by for the ribbon thickness.",
)
wavelength: float = Field(
default=10.0,
description="Factor to multiply linewidth by for the zigzag wavelength.",
)
amplitude: float = Field(
default=3.0,
description="Factor to multiply linewidth by for the zigzag amplitude.",
)
min_helix_length: int = Field(
default=4,
ge=2,
description="Minimum number of residues required to draw a zigzag shape instead of a line.",
)
|
options:
show_root_heading: true
Bases: BaseStructureSceneElement[SheetStyle]
Represents a Beta Sheet segment, visualized as a triangular arrow.
Source code in src/flatprot/scene/structure/sheet.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280 | class SheetSceneElement(BaseStructureSceneElement[SheetStyle]):
"""Represents a Beta Sheet segment, visualized as a triangular arrow."""
def __init__(
self,
residue_range_set: ResidueRangeSet,
style: Optional[SheetStyle] = None,
parent: Optional[SceneGroupType] = None,
):
"""Initializes the SheetSceneElement."""
super().__init__(residue_range_set, style, parent)
# Cache for the calculated arrow coordinates and original length
self._cached_display_coords: Optional[np.ndarray] = None
self._original_coords_len: Optional[int] = None
@property
def default_style(self) -> SheetStyle:
"""Provides the default style for Sheet elements."""
return SheetStyle()
def _get_original_coords_slice(self, structure: Structure) -> Optional[np.ndarray]:
"""Helper to extract the original coordinate slice for this sheet."""
coords_list = []
if not self.residue_range_set.ranges:
raise CoordinateCalculationError(
f"Cannot get coordinates for Sheet '{self.id}': no residue ranges defined."
)
sheet_range = self.residue_range_set.ranges[0]
try:
chain = structure[sheet_range.chain_id]
for res_idx in range(sheet_range.start, sheet_range.end + 1):
if res_idx in chain:
coord_idx = chain.coordinate_index(res_idx)
if 0 <= coord_idx < len(structure.coordinates):
coords_list.append(structure.coordinates[coord_idx])
else:
raise CoordinateCalculationError(
f"Sheet '{self.id}': Coordinate index {coord_idx} out of bounds for residue {sheet_range.chain_id}:{res_idx}."
)
else:
raise CoordinateCalculationError(
f"Sheet '{self.id}': Residue {sheet_range.chain_id}:{res_idx} not found in chain coordinate map."
)
except (KeyError, IndexError, AttributeError) as e:
raise CoordinateCalculationError(
f"Error fetching coordinates for sheet '{self.id}': {e}"
) from e
return np.array(coords_list) if coords_list else None
def get_coordinates(self, structure: Structure) -> Optional[np.ndarray]:
"""Retrieve the 2D + Depth coordinates for the sheet arrow.
Calculates the three points (arrow base left, base right, tip) based
on the start and end points of the pre-projected coordinate slice.
Handles minimum length requirement.
Args:
structure: The core Structure object containing pre-projected data.
Returns:
A NumPy array of the arrow coordinates (shape [3, 3] or [2, 3])
containing [X, Y, Depth] for each point.
"""
if self._cached_display_coords is not None:
return self._cached_display_coords
original_coords = self._get_original_coords_slice(structure)
if original_coords is None or len(original_coords) == 0:
self._cached_display_coords = None
self._original_coords_len = 0
return None
self._original_coords_len = len(original_coords)
# If only one point, cannot draw line or arrow
if self._original_coords_len == 1:
self._cached_display_coords = np.array(
[original_coords[0]]
) # Return the single point
return self._cached_display_coords
# Use only X, Y for shape calculation, keep Z (depth)
start_point_xy = original_coords[0, :2]
end_point_xy = original_coords[-1, :2]
# Use average depth of start/end for the base, end depth for tip
start_depth = original_coords[0, 2]
end_depth = original_coords[-1, 2]
avg_base_depth = (start_depth + end_depth) / 2.0
direction = end_point_xy - start_point_xy
length = np.linalg.norm(direction)
# If too short or degenerate, return a simple line (start and end points)
if length < 1e-6 or self._original_coords_len < self.style.min_sheet_length:
# Return original start and end points (X, Y, Depth)
self._cached_display_coords = np.array(
[original_coords[0], original_coords[-1]]
)
return self._cached_display_coords
# Normalize direction vector (only need X, Y)
direction /= length
# Calculate perpendicular vector in 2D
perp = np.array([-direction[1], direction[0]])
arrow_base_half_width = self.style.arrow_width / 2.0
# Calculate arrow base points (X, Y)
left_point_xy = start_point_xy + perp * arrow_base_half_width
right_point_xy = start_point_xy - perp * arrow_base_half_width
# Combine XY with Depth
left_point = np.append(left_point_xy, avg_base_depth)
right_point = np.append(right_point_xy, avg_base_depth)
tip_point = np.append(end_point_xy, end_depth) # Tip uses depth of last residue
self._cached_display_coords = np.array([left_point, right_point, tip_point])
return self._cached_display_coords
def get_coordinate_at_residue(
self, residue: ResidueCoordinate, structure: Structure
) -> Optional[np.ndarray]:
"""Retrieves the specific 2D coordinate + Depth corresponding to a residue
along the central axis of the sheet arrow representation.
Interpolates along the axis from the base midpoint to the tip, or along
the line if the arrow shape is not drawn.
Args:
residue: The residue coordinate (chain and index) to find the point for.
structure: The core Structure object containing pre-projected data.
Returns:
A NumPy array [X, Y, Depth] interpolated along the sheet axis.
"""
# 1. Ensure display coordinates are calculated and length is known
display_coords = self.get_coordinates(structure)
if (
display_coords is None
or self._original_coords_len is None
or self._original_coords_len == 0
):
return None
# 2. Check if residue is within the element's range
if residue not in self.residue_range_set:
return None
# Assuming single continuous range for sheet element representation
element_range = self.residue_range_set.ranges[0]
if residue.chain_id != element_range.chain_id:
return None
# 3. Calculate the 0-based index within the original sequence length
try:
original_sequence_index = residue.residue_index - element_range.start
# Validate index against the original length before simplification/arrow calc
if not (0 <= original_sequence_index < self._original_coords_len):
raise CoordinateCalculationError(
f"Residue index {original_sequence_index} derived from {residue} is out of original bounds [0, {self._original_coords_len}) for element {self.id}."
)
except Exception as e:
raise CoordinateCalculationError(
f"Error calculating original sequence index for {residue} in element {self.id}: {e}"
) from e
# Handle single point case
if self._original_coords_len == 1:
return display_coords[
0
] # Return the single point calculated by get_coordinates
# 4. Handle the case where a line was drawn (display_coords has 2 points)
if len(display_coords) == 2:
# Simple linear interpolation between the start and end points of the line
frac = original_sequence_index / (self._original_coords_len - 1)
interpolated_coord = (
display_coords[0] * (1 - frac) + display_coords[1] * frac
)
return interpolated_coord
# 5. Interpolate along the arrow axis (base midpoint to tip)
# display_coords has shape [3, 3]: [left_base, right_base, tip]
base_midpoint = (display_coords[0] + display_coords[1]) / 2.0
tip_point = display_coords[2]
# Calculate fraction along the length (0 = base midpoint, 1 = tip)
# Based on position within the *original* sequence length
frac = original_sequence_index / (self._original_coords_len - 1)
# Linear interpolation between base midpoint and tip point
interpolated_coord = base_midpoint * (1 - frac) + tip_point * frac
return interpolated_coord
def get_start_connection_point(self, structure: Structure) -> Optional[np.ndarray]:
"""Calculate the 2D coordinate for the start connection point.
Args:
structure: The core Structure object containing projected coordinates.
Returns:
A NumPy array [X, Y] or None if calculation fails.
"""
coords_2d = self.get_coordinates(structure)[:, :2]
if coords_2d is None:
return None
if len(coords_2d) < 3:
return coords_2d[0, :2]
return (coords_2d[0, :2] + coords_2d[1, :2]) / 2
def get_end_connection_point(self, structure: Structure) -> Optional[np.ndarray]:
"""Calculate the 2D coordinate for the end connection point.
Args:
structure: The core Structure object containing projected coordinates.
Returns:
A NumPy array [X, Y] or None if calculation fails.
"""
coords_2d = self.get_coordinates(structure)[:, :2]
if coords_2d is None:
return None
return coords_2d[-1, :2]
|
default_style
property
Provides the default style for Sheet elements.
__init__(residue_range_set, style=None, parent=None)
Initializes the SheetSceneElement.
Source code in src/flatprot/scene/structure/sheet.py
57
58
59
60
61
62
63
64
65
66
67 | def __init__(
self,
residue_range_set: ResidueRangeSet,
style: Optional[SheetStyle] = None,
parent: Optional[SceneGroupType] = None,
):
"""Initializes the SheetSceneElement."""
super().__init__(residue_range_set, style, parent)
# Cache for the calculated arrow coordinates and original length
self._cached_display_coords: Optional[np.ndarray] = None
self._original_coords_len: Optional[int] = None
|
get_coordinate_at_residue(residue, structure)
Retrieves the specific 2D coordinate + Depth corresponding to a residue
along the central axis of the sheet arrow representation.
Interpolates along the axis from the base midpoint to the tip, or along
the line if the arrow shape is not drawn.
Parameters: |
-
residue
(ResidueCoordinate )
–
The residue coordinate (chain and index) to find the point for.
-
structure
(Structure )
–
The core Structure object containing pre-projected data.
|
Returns: |
-
Optional[ndarray]
–
A NumPy array [X, Y, Depth] interpolated along the sheet axis.
|
Source code in src/flatprot/scene/structure/sheet.py
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249 | def get_coordinate_at_residue(
self, residue: ResidueCoordinate, structure: Structure
) -> Optional[np.ndarray]:
"""Retrieves the specific 2D coordinate + Depth corresponding to a residue
along the central axis of the sheet arrow representation.
Interpolates along the axis from the base midpoint to the tip, or along
the line if the arrow shape is not drawn.
Args:
residue: The residue coordinate (chain and index) to find the point for.
structure: The core Structure object containing pre-projected data.
Returns:
A NumPy array [X, Y, Depth] interpolated along the sheet axis.
"""
# 1. Ensure display coordinates are calculated and length is known
display_coords = self.get_coordinates(structure)
if (
display_coords is None
or self._original_coords_len is None
or self._original_coords_len == 0
):
return None
# 2. Check if residue is within the element's range
if residue not in self.residue_range_set:
return None
# Assuming single continuous range for sheet element representation
element_range = self.residue_range_set.ranges[0]
if residue.chain_id != element_range.chain_id:
return None
# 3. Calculate the 0-based index within the original sequence length
try:
original_sequence_index = residue.residue_index - element_range.start
# Validate index against the original length before simplification/arrow calc
if not (0 <= original_sequence_index < self._original_coords_len):
raise CoordinateCalculationError(
f"Residue index {original_sequence_index} derived from {residue} is out of original bounds [0, {self._original_coords_len}) for element {self.id}."
)
except Exception as e:
raise CoordinateCalculationError(
f"Error calculating original sequence index for {residue} in element {self.id}: {e}"
) from e
# Handle single point case
if self._original_coords_len == 1:
return display_coords[
0
] # Return the single point calculated by get_coordinates
# 4. Handle the case where a line was drawn (display_coords has 2 points)
if len(display_coords) == 2:
# Simple linear interpolation between the start and end points of the line
frac = original_sequence_index / (self._original_coords_len - 1)
interpolated_coord = (
display_coords[0] * (1 - frac) + display_coords[1] * frac
)
return interpolated_coord
# 5. Interpolate along the arrow axis (base midpoint to tip)
# display_coords has shape [3, 3]: [left_base, right_base, tip]
base_midpoint = (display_coords[0] + display_coords[1]) / 2.0
tip_point = display_coords[2]
# Calculate fraction along the length (0 = base midpoint, 1 = tip)
# Based on position within the *original* sequence length
frac = original_sequence_index / (self._original_coords_len - 1)
# Linear interpolation between base midpoint and tip point
interpolated_coord = base_midpoint * (1 - frac) + tip_point * frac
return interpolated_coord
|
get_coordinates(structure)
Retrieve the 2D + Depth coordinates for the sheet arrow.
Calculates the three points (arrow base left, base right, tip) based
on the start and end points of the pre-projected coordinate slice.
Handles minimum length requirement.
Parameters: |
-
structure
(Structure )
–
The core Structure object containing pre-projected data.
|
Returns: |
-
Optional[ndarray]
–
A NumPy array of the arrow coordinates (shape [3, 3] or [2, 3])
-
Optional[ndarray]
–
containing [X, Y, Depth] for each point.
|
Source code in src/flatprot/scene/structure/sheet.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174 | def get_coordinates(self, structure: Structure) -> Optional[np.ndarray]:
"""Retrieve the 2D + Depth coordinates for the sheet arrow.
Calculates the three points (arrow base left, base right, tip) based
on the start and end points of the pre-projected coordinate slice.
Handles minimum length requirement.
Args:
structure: The core Structure object containing pre-projected data.
Returns:
A NumPy array of the arrow coordinates (shape [3, 3] or [2, 3])
containing [X, Y, Depth] for each point.
"""
if self._cached_display_coords is not None:
return self._cached_display_coords
original_coords = self._get_original_coords_slice(structure)
if original_coords is None or len(original_coords) == 0:
self._cached_display_coords = None
self._original_coords_len = 0
return None
self._original_coords_len = len(original_coords)
# If only one point, cannot draw line or arrow
if self._original_coords_len == 1:
self._cached_display_coords = np.array(
[original_coords[0]]
) # Return the single point
return self._cached_display_coords
# Use only X, Y for shape calculation, keep Z (depth)
start_point_xy = original_coords[0, :2]
end_point_xy = original_coords[-1, :2]
# Use average depth of start/end for the base, end depth for tip
start_depth = original_coords[0, 2]
end_depth = original_coords[-1, 2]
avg_base_depth = (start_depth + end_depth) / 2.0
direction = end_point_xy - start_point_xy
length = np.linalg.norm(direction)
# If too short or degenerate, return a simple line (start and end points)
if length < 1e-6 or self._original_coords_len < self.style.min_sheet_length:
# Return original start and end points (X, Y, Depth)
self._cached_display_coords = np.array(
[original_coords[0], original_coords[-1]]
)
return self._cached_display_coords
# Normalize direction vector (only need X, Y)
direction /= length
# Calculate perpendicular vector in 2D
perp = np.array([-direction[1], direction[0]])
arrow_base_half_width = self.style.arrow_width / 2.0
# Calculate arrow base points (X, Y)
left_point_xy = start_point_xy + perp * arrow_base_half_width
right_point_xy = start_point_xy - perp * arrow_base_half_width
# Combine XY with Depth
left_point = np.append(left_point_xy, avg_base_depth)
right_point = np.append(right_point_xy, avg_base_depth)
tip_point = np.append(end_point_xy, end_depth) # Tip uses depth of last residue
self._cached_display_coords = np.array([left_point, right_point, tip_point])
return self._cached_display_coords
|
get_end_connection_point(structure)
Calculate the 2D coordinate for the end connection point.
Parameters: |
-
structure
(Structure )
–
The core Structure object containing projected coordinates.
|
Returns: |
-
Optional[ndarray]
–
A NumPy array [X, Y] or None if calculation fails.
|
Source code in src/flatprot/scene/structure/sheet.py
268
269
270
271
272
273
274
275
276
277
278
279
280 | def get_end_connection_point(self, structure: Structure) -> Optional[np.ndarray]:
"""Calculate the 2D coordinate for the end connection point.
Args:
structure: The core Structure object containing projected coordinates.
Returns:
A NumPy array [X, Y] or None if calculation fails.
"""
coords_2d = self.get_coordinates(structure)[:, :2]
if coords_2d is None:
return None
return coords_2d[-1, :2]
|
get_start_connection_point(structure)
Calculate the 2D coordinate for the start connection point.
Parameters: |
-
structure
(Structure )
–
The core Structure object containing projected coordinates.
|
Returns: |
-
Optional[ndarray]
–
A NumPy array [X, Y] or None if calculation fails.
|
Source code in src/flatprot/scene/structure/sheet.py
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266 | def get_start_connection_point(self, structure: Structure) -> Optional[np.ndarray]:
"""Calculate the 2D coordinate for the start connection point.
Args:
structure: The core Structure object containing projected coordinates.
Returns:
A NumPy array [X, Y] or None if calculation fails.
"""
coords_2d = self.get_coordinates(structure)[:, :2]
if coords_2d is None:
return None
if len(coords_2d) < 3:
return coords_2d[0, :2]
return (coords_2d[0, :2] + coords_2d[1, :2]) / 2
|
options:
show_root_heading: true
Bases: BaseStructureStyle
Style properties specific to Sheet elements.
Defines properties for rendering beta sheets as triangular arrows.
Source code in src/flatprot/scene/structure/sheet.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 | class SheetStyle(BaseStructureStyle):
"""Style properties specific to Sheet elements.
Defines properties for rendering beta sheets as triangular arrows.
"""
# Override inherited defaults
color: Color = Field(
default=Color("#0000ff"), description="Default color for sheet (blue)."
)
stroke_width: float = Field(
default=1.0, description="Base width of the sheet arrow."
)
simplified_width: float = Field(
default=2,
description="Width to use for simplified sheet rendering (line only).",
)
# Sheet-specific attributes
arrow_width: float = Field(
default=8.0,
description="Factor to multiply linewidth by for the arrowhead base width.",
)
min_sheet_length: int = Field(
default=3,
ge=1,
description="Minimum number of residues required to draw an arrow shape instead of a line.",
)
|
options:
show_root_heading: true
Bases: BaseStructureSceneElement[CoilStyle]
Represents a Coil segment of a protein structure.
Renders as a smoothed line based on the pre-projected coordinates.
Source code in src/flatprot/scene/structure/coil.py
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272 | class CoilSceneElement(BaseStructureSceneElement[CoilStyle]):
"""Represents a Coil segment of a protein structure.
Renders as a smoothed line based on the pre-projected coordinates.
"""
def __init__(
self,
residue_range_set: ResidueRangeSet,
style: Optional[CoilStyle] = None,
parent: Optional[SceneGroupType] = None,
):
"""Initializes the CoilSceneElement."""
super().__init__(residue_range_set, style, parent)
# Cache for the calculated smoothed coordinates and original indices
self._cached_smoothed_coords: Optional[np.ndarray] = None
self._original_indices: Optional[np.ndarray] = None
self._original_coords_len: Optional[int] = None
@property
def default_style(self) -> CoilStyle:
"""Provides the default style for Coil elements."""
return CoilStyle()
def _get_original_coords_slice(self, structure: Structure) -> Optional[np.ndarray]:
"""Helper to extract the original coordinate slice for this coil."""
coords_list = []
if not self.residue_range_set.ranges:
raise CoordinateCalculationError(
f"Cannot get coordinates for Coil '{self.id}': no residue ranges defined."
)
coil_range = self.residue_range_set.ranges[0]
try:
chain = structure[coil_range.chain_id]
for res_idx in range(coil_range.start, coil_range.end + 1):
if res_idx in chain:
coord_idx = chain.coordinate_index(res_idx)
if 0 <= coord_idx < len(structure.coordinates):
coords_list.append(structure.coordinates[coord_idx])
else:
raise CoordinateCalculationError(
f"Coil '{self.id}': Coordinate index {coord_idx} out of bounds for residue {coil_range.chain_id}:{res_idx}."
)
else:
raise CoordinateCalculationError(
f"Coil '{self.id}': Residue {coil_range.chain_id}:{res_idx} not found in chain coordinate map."
)
except (KeyError, IndexError, AttributeError) as e:
raise CoordinateCalculationError(
f"Error getting original coordinates for Coil '{self.id}': {e}"
) from e
return np.array(coords_list) if coords_list else None
def get_coordinates(self, structure: Structure) -> Optional[np.ndarray]:
"""Retrieve the smoothed 2D + Depth coordinates for rendering the coil.
Fetches the pre-projected coordinates from the structure, applies smoothing
based on the style's smoothing_factor, and caches the result.
Args:
structure: The core Structure object containing pre-projected data.
Returns:
A NumPy array of smoothed 2D + Depth coordinates (X, Y, Depth).
"""
# Return cached result if available
if self._cached_smoothed_coords is not None:
return self._cached_smoothed_coords
# 1. Get the original (pre-projected) coordinates slice for this element
original_coords = self._get_original_coords_slice(structure)
if original_coords is None:
self._cached_smoothed_coords = None
self._original_indices = None
self._original_coords_len = 0
return None
self._original_coords_len = len(original_coords)
# Handle single-point coils separately
if self._original_coords_len == 1:
self._cached_smoothed_coords = original_coords
self._original_indices = np.array([0]) # Index of the single point
return self._cached_smoothed_coords
# 2. Apply smoothing based on style (only if >= 2 points)
smoothing_factor = self.style.smoothing_factor
smoothed_coords, used_indices = smooth_coordinates(
original_coords, smoothing_factor
)
# 3. Cache and return
self._cached_smoothed_coords = smoothed_coords
# Map the indices from smooth_coordinates (relative to the slice) back to the
# original residue indices or coordinate indices if needed elsewhere, but
# for get_2d_coordinate_at_residue, we primarily need the mapping *between*
# original sequence index and smoothed sequence index.
# We store the indices *within the original slice* that were kept.
self._original_indices = used_indices
return self._cached_smoothed_coords
def get_coordinate_at_residue(
self, residue: ResidueCoordinate, structure: Structure
) -> Optional[np.ndarray]:
"""Retrieves the specific 2D coordinate + Depth corresponding to a residue
within the smoothed representation of the coil.
Uses linear interpolation between the points of the smoothed coil line.
Args:
residue: The residue coordinate (chain and index) to find the 2D point for.
structure: The core Structure object containing pre-projected 2D + Depth data.
Returns:
A NumPy array [X, Y, Depth] from the smoothed representation, potentially interpolated.
"""
# 1. Ensure smoothed coordinates are calculated and cached
# This call populates self._cached_smoothed_coords, self._original_coords_len, etc.
smoothed_coords = self.get_coordinates(structure)
if smoothed_coords is None or self._original_coords_len is None:
return None # Cannot determine coordinate if smoothing failed
# 2. Check if residue is within the element's range
if residue not in self.residue_range_set:
return None
# Assuming single range for simplicity
element_range = self.residue_range_set.ranges[0]
if residue.chain_id != element_range.chain_id:
return None
# 3. Map residue index to the 0-based index within the *original* sequence of this coil
# This index represents the position *before* smoothing.
try:
original_sequence_index = residue.residue_index - element_range.start
if not (0 <= original_sequence_index < self._original_coords_len):
return None # Residue index is outside the valid range for this element
except Exception:
return None # Should not happen if residue is in range set, but defensive check
# 4. Map the original sequence index to the fractional index within the *smoothed* sequence
# This tells us where the original residue falls along the smoothed line.
orig_len = self._original_coords_len
smooth_len = len(smoothed_coords)
# Avoid division by zero if original length was 1 (although checked earlier)
if orig_len <= 1:
return smoothed_coords[0] if smooth_len > 0 else None
# Calculate fractional position along the smoothed line
mapped_idx_frac = (original_sequence_index * (smooth_len - 1)) / (orig_len - 1)
# 5. Linear interpolation between adjacent smoothed points
idx_low = int(np.floor(mapped_idx_frac))
# Clamp idx_high to the last valid index of the smoothed array
idx_high = min(idx_low + 1, smooth_len - 1)
# Ensure idx_low is also within bounds (handles edge case where mapped_idx_frac might be exactly smooth_len-1)
idx_low = min(idx_low, smooth_len - 1)
# Calculate interpolation fraction
frac = mapped_idx_frac - idx_low
# Interpolate X, Y, and Depth
coord_low = smoothed_coords[idx_low]
coord_high = smoothed_coords[idx_high]
interpolated_coord = coord_low * (1 - frac) + coord_high * frac
return interpolated_coord
def get_start_connection_point(self, structure: Structure) -> Optional[np.ndarray]:
"""Calculate the 2D coordinate for the start connection point.
Args:
structure: The core Structure object containing projected coordinates.
Returns:
A NumPy array [X, Y] or None if calculation fails.
"""
coords_2d = self.get_coordinates(structure)[:, :2]
if coords_2d is None:
return None
return coords_2d[0, :2]
def get_end_connection_point(self, structure: Structure) -> Optional[np.ndarray]:
"""Calculate the 2D coordinate for the end connection point.
Args:
structure: The core Structure object containing projected coordinates.
Returns:
A NumPy array [X, Y] or None if calculation fails.
"""
coords_2d = self.get_coordinates(structure)[:, :2]
if coords_2d is None:
return None
return coords_2d[-1, :2]
|
default_style
property
Provides the default style for Coil elements.
__init__(residue_range_set, style=None, parent=None)
Initializes the CoilSceneElement.
Source code in src/flatprot/scene/structure/coil.py
80
81
82
83
84
85
86
87
88
89
90
91 | def __init__(
self,
residue_range_set: ResidueRangeSet,
style: Optional[CoilStyle] = None,
parent: Optional[SceneGroupType] = None,
):
"""Initializes the CoilSceneElement."""
super().__init__(residue_range_set, style, parent)
# Cache for the calculated smoothed coordinates and original indices
self._cached_smoothed_coords: Optional[np.ndarray] = None
self._original_indices: Optional[np.ndarray] = None
self._original_coords_len: Optional[int] = None
|
get_coordinate_at_residue(residue, structure)
Retrieves the specific 2D coordinate + Depth corresponding to a residue
within the smoothed representation of the coil.
Uses linear interpolation between the points of the smoothed coil line.
Parameters: |
-
residue
(ResidueCoordinate )
–
The residue coordinate (chain and index) to find the 2D point for.
-
structure
(Structure )
–
The core Structure object containing pre-projected 2D + Depth data.
|
Returns: |
-
Optional[ndarray]
–
A NumPy array [X, Y, Depth] from the smoothed representation, potentially interpolated.
|
Source code in src/flatprot/scene/structure/coil.py
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243 | def get_coordinate_at_residue(
self, residue: ResidueCoordinate, structure: Structure
) -> Optional[np.ndarray]:
"""Retrieves the specific 2D coordinate + Depth corresponding to a residue
within the smoothed representation of the coil.
Uses linear interpolation between the points of the smoothed coil line.
Args:
residue: The residue coordinate (chain and index) to find the 2D point for.
structure: The core Structure object containing pre-projected 2D + Depth data.
Returns:
A NumPy array [X, Y, Depth] from the smoothed representation, potentially interpolated.
"""
# 1. Ensure smoothed coordinates are calculated and cached
# This call populates self._cached_smoothed_coords, self._original_coords_len, etc.
smoothed_coords = self.get_coordinates(structure)
if smoothed_coords is None or self._original_coords_len is None:
return None # Cannot determine coordinate if smoothing failed
# 2. Check if residue is within the element's range
if residue not in self.residue_range_set:
return None
# Assuming single range for simplicity
element_range = self.residue_range_set.ranges[0]
if residue.chain_id != element_range.chain_id:
return None
# 3. Map residue index to the 0-based index within the *original* sequence of this coil
# This index represents the position *before* smoothing.
try:
original_sequence_index = residue.residue_index - element_range.start
if not (0 <= original_sequence_index < self._original_coords_len):
return None # Residue index is outside the valid range for this element
except Exception:
return None # Should not happen if residue is in range set, but defensive check
# 4. Map the original sequence index to the fractional index within the *smoothed* sequence
# This tells us where the original residue falls along the smoothed line.
orig_len = self._original_coords_len
smooth_len = len(smoothed_coords)
# Avoid division by zero if original length was 1 (although checked earlier)
if orig_len <= 1:
return smoothed_coords[0] if smooth_len > 0 else None
# Calculate fractional position along the smoothed line
mapped_idx_frac = (original_sequence_index * (smooth_len - 1)) / (orig_len - 1)
# 5. Linear interpolation between adjacent smoothed points
idx_low = int(np.floor(mapped_idx_frac))
# Clamp idx_high to the last valid index of the smoothed array
idx_high = min(idx_low + 1, smooth_len - 1)
# Ensure idx_low is also within bounds (handles edge case where mapped_idx_frac might be exactly smooth_len-1)
idx_low = min(idx_low, smooth_len - 1)
# Calculate interpolation fraction
frac = mapped_idx_frac - idx_low
# Interpolate X, Y, and Depth
coord_low = smoothed_coords[idx_low]
coord_high = smoothed_coords[idx_high]
interpolated_coord = coord_low * (1 - frac) + coord_high * frac
return interpolated_coord
|
get_coordinates(structure)
Retrieve the smoothed 2D + Depth coordinates for rendering the coil.
Fetches the pre-projected coordinates from the structure, applies smoothing
based on the style's smoothing_factor, and caches the result.
Parameters: |
-
structure
(Structure )
–
The core Structure object containing pre-projected data.
|
Returns: |
-
Optional[ndarray]
–
A NumPy array of smoothed 2D + Depth coordinates (X, Y, Depth).
|
Source code in src/flatprot/scene/structure/coil.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176 | def get_coordinates(self, structure: Structure) -> Optional[np.ndarray]:
"""Retrieve the smoothed 2D + Depth coordinates for rendering the coil.
Fetches the pre-projected coordinates from the structure, applies smoothing
based on the style's smoothing_factor, and caches the result.
Args:
structure: The core Structure object containing pre-projected data.
Returns:
A NumPy array of smoothed 2D + Depth coordinates (X, Y, Depth).
"""
# Return cached result if available
if self._cached_smoothed_coords is not None:
return self._cached_smoothed_coords
# 1. Get the original (pre-projected) coordinates slice for this element
original_coords = self._get_original_coords_slice(structure)
if original_coords is None:
self._cached_smoothed_coords = None
self._original_indices = None
self._original_coords_len = 0
return None
self._original_coords_len = len(original_coords)
# Handle single-point coils separately
if self._original_coords_len == 1:
self._cached_smoothed_coords = original_coords
self._original_indices = np.array([0]) # Index of the single point
return self._cached_smoothed_coords
# 2. Apply smoothing based on style (only if >= 2 points)
smoothing_factor = self.style.smoothing_factor
smoothed_coords, used_indices = smooth_coordinates(
original_coords, smoothing_factor
)
# 3. Cache and return
self._cached_smoothed_coords = smoothed_coords
# Map the indices from smooth_coordinates (relative to the slice) back to the
# original residue indices or coordinate indices if needed elsewhere, but
# for get_2d_coordinate_at_residue, we primarily need the mapping *between*
# original sequence index and smoothed sequence index.
# We store the indices *within the original slice* that were kept.
self._original_indices = used_indices
return self._cached_smoothed_coords
|
get_end_connection_point(structure)
Calculate the 2D coordinate for the end connection point.
Parameters: |
-
structure
(Structure )
–
The core Structure object containing projected coordinates.
|
Returns: |
-
Optional[ndarray]
–
A NumPy array [X, Y] or None if calculation fails.
|
Source code in src/flatprot/scene/structure/coil.py
260
261
262
263
264
265
266
267
268
269
270
271
272 | def get_end_connection_point(self, structure: Structure) -> Optional[np.ndarray]:
"""Calculate the 2D coordinate for the end connection point.
Args:
structure: The core Structure object containing projected coordinates.
Returns:
A NumPy array [X, Y] or None if calculation fails.
"""
coords_2d = self.get_coordinates(structure)[:, :2]
if coords_2d is None:
return None
return coords_2d[-1, :2]
|
get_start_connection_point(structure)
Calculate the 2D coordinate for the start connection point.
Parameters: |
-
structure
(Structure )
–
The core Structure object containing projected coordinates.
|
Returns: |
-
Optional[ndarray]
–
A NumPy array [X, Y] or None if calculation fails.
|
Source code in src/flatprot/scene/structure/coil.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258 | def get_start_connection_point(self, structure: Structure) -> Optional[np.ndarray]:
"""Calculate the 2D coordinate for the start connection point.
Args:
structure: The core Structure object containing projected coordinates.
Returns:
A NumPy array [X, Y] or None if calculation fails.
"""
coords_2d = self.get_coordinates(structure)[:, :2]
if coords_2d is None:
return None
return coords_2d[0, :2]
|
options:
show_root_heading: true
Bases: BaseStructureStyle
Style properties specific to Coil elements.
Source code in src/flatprot/scene/structure/coil.py
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70 | class CoilStyle(BaseStructureStyle):
"""Style properties specific to Coil elements."""
# Override inherited defaults
color: Color = Field(
default=Color("#5b5859"),
description="Default color for coil (light grey).",
)
stroke_width: float = Field(default=1.0, description="Line width for coil.")
# Coil-specific attribute
smoothing_factor: float = Field(
default=0.1,
ge=0.0,
le=1.0,
description="Fraction of points to keep during smoothing (0.0 to 1.0)."
"Higher value means less smoothing.",
)
|
options:
show_root_heading: true
Annotation Elements
Classes representing annotation elements within the scene.
Bases: BaseSceneElement[AnnotationStyleType]
, ABC
, Generic[AnnotationStyleType]
Abstract base class for scene elements representing annotations.
Stores the original target specification (coordinates, range, or range set)
and requires the corresponding ResidueRangeSet for the base scene element.
Requires a concrete style type inheriting from BaseAnnotationStyle.
Source code in src/flatprot/scene/annotation/base_annotation.py
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179 | class BaseAnnotationElement(
BaseSceneElement[AnnotationStyleType], ABC, Generic[AnnotationStyleType]
):
"""Abstract base class for scene elements representing annotations.
Stores the original target specification (coordinates, range, or range set)
and requires the corresponding ResidueRangeSet for the base scene element.
Requires a concrete style type inheriting from BaseAnnotationStyle.
"""
def __init__(
self,
id: str, # ID is required for annotations
target: Union[
ResidueCoordinate, List[ResidueCoordinate], ResidueRange, ResidueRangeSet
],
label: Optional[str] = None,
style: Optional[AnnotationStyleType] = None,
parent: Optional[SceneGroupType] = None,
):
"""Initializes a BaseAnnotationElement.
Subclasses are responsible for constructing the appropriate `residue_range_set`
based on their specific `target` type before calling this initializer.
Args:
id: A unique identifier for this annotation element.
target: The original target specification (list of coordinates, range, or set).
Stored for use by subclasses in `get_coordinates`.
residue_range_set: The ResidueRangeSet derived from the target, required by
the BaseSceneElement for its internal logic (e.g., bounding box).
label: The label for the annotation.
style: An optional specific style instance for this annotation.
parent: The parent SceneGroup in the scene graph, if any.
Raises:
TypeError: If the target type is not one of the allowed types.
ValueError: If residue_range_set is empty.
"""
# Validate the target type
if not isinstance(
target, (ResidueCoordinate, list, ResidueRange, ResidueRangeSet)
) or (
isinstance(target, list)
and not all(isinstance(item, ResidueCoordinate) for item in target)
):
raise ValueError(
f"Unsupported target type for annotation: {type(target)}. "
f"Expected List[ResidueCoordinate], ResidueRange, or ResidueRangeSet."
)
self.label = label
self._target = target # Store the original target
# Pass the explicitly provided residue_range_set to the BaseSceneElement constructor
super().__init__(
id=id,
style=style,
parent=parent,
)
@property
def target(self) -> Union[List[ResidueCoordinate], ResidueRange, ResidueRangeSet]:
"""Get the target specification provided during initialization."""
return self._target
@property
def targets_specific_coordinates(self) -> bool:
"""Check if this annotation targets a list of specific coordinates."""
return isinstance(self._target, list)
@abstractmethod
def get_coordinates(self, resolver: CoordinateResolver) -> np.ndarray:
"""Calculate the renderable coordinates for this annotation.
Uses the provided CoordinateResolver to find the correct coordinates for
its target (coordinates, range, or range set) in the context of the scene elements.
The interpretation of the target depends on the concrete annotation type.
Args:
resolver: The CoordinateResolver instance for the scene.
Returns:
A NumPy array of coordinates (shape [N, 3], X, Y, Z) suitable for rendering.
Raises:
CoordinateCalculationError: If coordinates cannot be resolved.
TargetResidueNotFoundError: If a target residue is not found.
# Other specific exceptions possible depending on implementation
"""
raise NotImplementedError
# Concrete subclasses (Marker, Line, Area) MUST implement default_style
@property
@abstractmethod
def default_style(self) -> AnnotationStyleType:
"""Provides the default style instance for this specific annotation type.
Concrete subclasses must implement this property.
Returns:
An instance of the specific AnnotationStyleType for this element.
"""
raise NotImplementedError
def get_depth(self, structure: Structure) -> Optional[float]:
"""Return a fixed high depth value for annotations.
This ensures annotations are rendered on top of other elements
when sorted by depth (ascending).
Args:
structure: The core Structure object (unused).
Returns:
A very large float value (infinity).
"""
# Return positive infinity to ensure annotations are sorted last (drawn on top)
# when using ascending sort order for depth. Adjust if sort order is descending.
return float("inf")
|
default_style
abstractmethod
property
Provides the default style instance for this specific annotation type.
Concrete subclasses must implement this property.
Returns: |
-
AnnotationStyleType
–
An instance of the specific AnnotationStyleType for this element.
|
target
property
Get the target specification provided during initialization.
targets_specific_coordinates
property
Check if this annotation targets a list of specific coordinates.
__init__(id, target, label=None, style=None, parent=None)
Initializes a BaseAnnotationElement.
Subclasses are responsible for constructing the appropriate residue_range_set
based on their specific target
type before calling this initializer.
Parameters: |
-
id
(str )
–
A unique identifier for this annotation element.
-
target
(Union[ResidueCoordinate, List[ResidueCoordinate], ResidueRange, ResidueRangeSet] )
–
The original target specification (list of coordinates, range, or set).
Stored for use by subclasses in get_coordinates .
-
residue_range_set
–
The ResidueRangeSet derived from the target, required by
the BaseSceneElement for its internal logic (e.g., bounding box).
-
label
(Optional[str] , default:
None
)
–
The label for the annotation.
-
style
(Optional[AnnotationStyleType] , default:
None
)
–
An optional specific style instance for this annotation.
-
parent
(Optional[SceneGroupType] , default:
None
)
–
The parent SceneGroup in the scene graph, if any.
|
Raises: |
-
TypeError
–
If the target type is not one of the allowed types.
-
ValueError
–
If residue_range_set is empty.
|
Source code in src/flatprot/scene/annotation/base_annotation.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119 | def __init__(
self,
id: str, # ID is required for annotations
target: Union[
ResidueCoordinate, List[ResidueCoordinate], ResidueRange, ResidueRangeSet
],
label: Optional[str] = None,
style: Optional[AnnotationStyleType] = None,
parent: Optional[SceneGroupType] = None,
):
"""Initializes a BaseAnnotationElement.
Subclasses are responsible for constructing the appropriate `residue_range_set`
based on their specific `target` type before calling this initializer.
Args:
id: A unique identifier for this annotation element.
target: The original target specification (list of coordinates, range, or set).
Stored for use by subclasses in `get_coordinates`.
residue_range_set: The ResidueRangeSet derived from the target, required by
the BaseSceneElement for its internal logic (e.g., bounding box).
label: The label for the annotation.
style: An optional specific style instance for this annotation.
parent: The parent SceneGroup in the scene graph, if any.
Raises:
TypeError: If the target type is not one of the allowed types.
ValueError: If residue_range_set is empty.
"""
# Validate the target type
if not isinstance(
target, (ResidueCoordinate, list, ResidueRange, ResidueRangeSet)
) or (
isinstance(target, list)
and not all(isinstance(item, ResidueCoordinate) for item in target)
):
raise ValueError(
f"Unsupported target type for annotation: {type(target)}. "
f"Expected List[ResidueCoordinate], ResidueRange, or ResidueRangeSet."
)
self.label = label
self._target = target # Store the original target
# Pass the explicitly provided residue_range_set to the BaseSceneElement constructor
super().__init__(
id=id,
style=style,
parent=parent,
)
|
get_coordinates(resolver)
abstractmethod
Calculate the renderable coordinates for this annotation.
Uses the provided CoordinateResolver to find the correct coordinates for
its target (coordinates, range, or range set) in the context of the scene elements.
The interpretation of the target depends on the concrete annotation type.
Returns: |
-
ndarray
–
A NumPy array of coordinates (shape [N, 3], X, Y, Z) suitable for rendering.
|
Source code in src/flatprot/scene/annotation/base_annotation.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150 | @abstractmethod
def get_coordinates(self, resolver: CoordinateResolver) -> np.ndarray:
"""Calculate the renderable coordinates for this annotation.
Uses the provided CoordinateResolver to find the correct coordinates for
its target (coordinates, range, or range set) in the context of the scene elements.
The interpretation of the target depends on the concrete annotation type.
Args:
resolver: The CoordinateResolver instance for the scene.
Returns:
A NumPy array of coordinates (shape [N, 3], X, Y, Z) suitable for rendering.
Raises:
CoordinateCalculationError: If coordinates cannot be resolved.
TargetResidueNotFoundError: If a target residue is not found.
# Other specific exceptions possible depending on implementation
"""
raise NotImplementedError
|
get_depth(structure)
Return a fixed high depth value for annotations.
This ensures annotations are rendered on top of other elements
when sorted by depth (ascending).
Parameters: |
-
structure
(Structure )
–
The core Structure object (unused).
|
Returns: |
-
Optional[float]
–
A very large float value (infinity).
|
Source code in src/flatprot/scene/annotation/base_annotation.py
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179 | def get_depth(self, structure: Structure) -> Optional[float]:
"""Return a fixed high depth value for annotations.
This ensures annotations are rendered on top of other elements
when sorted by depth (ascending).
Args:
structure: The core Structure object (unused).
Returns:
A very large float value (infinity).
"""
# Return positive infinity to ensure annotations are sorted last (drawn on top)
# when using ascending sort order for depth. Adjust if sort order is descending.
return float("inf")
|
options:
show_root_heading: true
Bases: BaseSceneStyle
Base style for annotation elements.
Source code in src/flatprot/scene/annotation/base_annotation.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57 | class BaseAnnotationStyle(BaseSceneStyle):
"""Base style for annotation elements."""
color: Color = Field(
default=Color((1.0, 0.0, 0.0)),
description="Default color for the annotation (hex string). Red.",
)
offset: Tuple[float, float] = Field(
default=(0.0, 0.0),
description="2D offset (x, y) from the anchor point in canvas units.",
)
label_offset: Tuple[float, float] = Field(
default=(0.0, 0.0),
description="2D offset (x, y) from the label anchor point in canvas units.",
)
label_color: Color = Field(
default=Color((0.0, 0.0, 0.0)),
description="Default color for the label (hex string). Black.",
)
label_font_size: float = Field(
default=12.0,
description="Font size for the label.",
)
label_font_weight: str = Field(
default="normal",
description="Font weight for the label.",
)
label_font_family: str = Field(
default="Arial",
description="Font family for the label.",
)
label: Optional[str] = Field(
default=None, description="Optional text label for the annotation."
)
|
options:
show_root_heading: true
Bases: BaseAnnotationElement[PointAnnotationStyle]
Represents an annotation marking a single residue coordinate.
Source code in src/flatprot/scene/annotation/point.py
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101 | class PointAnnotation(BaseAnnotationElement[PointAnnotationStyle]):
"""Represents an annotation marking a single residue coordinate."""
def __init__(
self,
id: str,
target: ResidueCoordinate, # Expects a single coordinate
label: Optional[str] = None,
style: Optional[PointAnnotationStyle] = None,
parent: Optional[SceneGroupType] = None,
):
"""Initializes a PointAnnotation.
Args:
id: A unique identifier for this annotation element.
target_coordinate: The specific residue coordinate this annotation targets.
style: An optional specific style instance for this annotation.
metadata: Optional dictionary for storing arbitrary metadata.
parent: The parent SceneGroup in the scene graph, if any.
"""
if not isinstance(target, ResidueCoordinate):
raise TypeError(
"target_coordinate must be a single ResidueCoordinate instance."
)
# Call superclass init, passing the single coordinate in a list
super().__init__(
id=id,
target=target, # Base class expects a list
style=style,
label=label,
parent=parent,
)
@property
def target_coordinate(self) -> ResidueCoordinate:
"""Get the specific target coordinate for this point annotation."""
# target_coordinates is guaranteed to be a list with one element by __init__
return self.target
@property
def default_style(self) -> PointAnnotationStyle:
"""Provides the default style for PointAnnotation elements."""
return PointAnnotationStyle()
def get_coordinates(self, resolver: CoordinateResolver) -> np.ndarray:
"""Calculate the coordinates for the point annotation marker.
Uses the CoordinateResolver to find the rendered coordinate of the target residue.
Args:
resolver: The CoordinateResolver instance for the scene.
Returns:
A NumPy array of shape [1, 3] containing the (X, Y, Z) coordinates
of the target point.
Raises:
CoordinateCalculationError: If the coordinate cannot be resolved.
TargetResidueNotFoundError: If the target residue is not found.
"""
target_res = self.target_coordinate
# Delegate resolution to the resolver
point = resolver.resolve(target_res)
# Resolver handles errors, so point should be valid if no exception was raised
return np.array([point])
|
default_style
property
Provides the default style for PointAnnotation elements.
target_coordinate
property
Get the specific target coordinate for this point annotation.
__init__(id, target, label=None, style=None, parent=None)
Initializes a PointAnnotation.
Parameters: |
-
id
(str )
–
A unique identifier for this annotation element.
-
target_coordinate
–
The specific residue coordinate this annotation targets.
-
style
(Optional[PointAnnotationStyle] , default:
None
)
–
An optional specific style instance for this annotation.
-
metadata
–
Optional dictionary for storing arbitrary metadata.
-
parent
(Optional[SceneGroupType] , default:
None
)
–
The parent SceneGroup in the scene graph, if any.
|
Source code in src/flatprot/scene/annotation/point.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68 | def __init__(
self,
id: str,
target: ResidueCoordinate, # Expects a single coordinate
label: Optional[str] = None,
style: Optional[PointAnnotationStyle] = None,
parent: Optional[SceneGroupType] = None,
):
"""Initializes a PointAnnotation.
Args:
id: A unique identifier for this annotation element.
target_coordinate: The specific residue coordinate this annotation targets.
style: An optional specific style instance for this annotation.
metadata: Optional dictionary for storing arbitrary metadata.
parent: The parent SceneGroup in the scene graph, if any.
"""
if not isinstance(target, ResidueCoordinate):
raise TypeError(
"target_coordinate must be a single ResidueCoordinate instance."
)
# Call superclass init, passing the single coordinate in a list
super().__init__(
id=id,
target=target, # Base class expects a list
style=style,
label=label,
parent=parent,
)
|
get_coordinates(resolver)
Calculate the coordinates for the point annotation marker.
Uses the CoordinateResolver to find the rendered coordinate of the target residue.
Returns: |
-
ndarray
–
A NumPy array of shape [1, 3] containing the (X, Y, Z) coordinates
-
ndarray
–
|
Raises: |
-
CoordinateCalculationError
–
If the coordinate cannot be resolved.
-
TargetResidueNotFoundError
–
If the target residue is not found.
|
Source code in src/flatprot/scene/annotation/point.py
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101 | def get_coordinates(self, resolver: CoordinateResolver) -> np.ndarray:
"""Calculate the coordinates for the point annotation marker.
Uses the CoordinateResolver to find the rendered coordinate of the target residue.
Args:
resolver: The CoordinateResolver instance for the scene.
Returns:
A NumPy array of shape [1, 3] containing the (X, Y, Z) coordinates
of the target point.
Raises:
CoordinateCalculationError: If the coordinate cannot be resolved.
TargetResidueNotFoundError: If the target residue is not found.
"""
target_res = self.target_coordinate
# Delegate resolution to the resolver
point = resolver.resolve(target_res)
# Resolver handles errors, so point should be valid if no exception was raised
return np.array([point])
|
options:
show_root_heading: true
Bases: BaseAnnotationStyle
Style properties specific to PointAnnotation elements.
Source code in src/flatprot/scene/annotation/point.py
| class PointAnnotationStyle(BaseAnnotationStyle):
"""Style properties specific to PointAnnotation elements."""
marker_radius: float = Field(
default=5.0,
ge=0,
description="Radius of the point marker.",
)
|
options:
show_root_heading: true
Bases: BaseAnnotationElement[LineAnnotationStyle]
Represents an annotation connecting two specific residue coordinates with a line.
Source code in src/flatprot/scene/annotation/line.py
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136 | class LineAnnotation(BaseAnnotationElement[LineAnnotationStyle]):
"""Represents an annotation connecting two specific residue coordinates with a line."""
def __init__(
self,
id: str,
start_coordinate: ResidueCoordinate,
end_coordinate: ResidueCoordinate,
style: Optional[LineAnnotationStyle] = None,
label: Optional[str] = None,
parent: Optional[SceneGroupType] = None,
):
"""Initializes a LineAnnotation.
Args:
id: A unique identifier for this annotation element.
target_coordinates: A list containing exactly two ResidueCoordinates
defining the start and end points of the line.
style: The specific style instance for this line annotation.
label: The label for the annotation.
parent: The parent SceneGroup in the scene graph, if any.
Raises:
ValueError: If `target_coordinates` does not contain exactly two elements.
TypeError: If elements in `target_coordinates` are not ResidueCoordinate instances.
"""
if not isinstance(start_coordinate, ResidueCoordinate) or not isinstance(
end_coordinate, ResidueCoordinate
):
raise ValueError(
"LineAnnotation must be initialized with two ResidueCoordinate instances."
)
# Call superclass init
super().__init__(
id=id,
target=[start_coordinate, end_coordinate],
style=style,
label=label,
parent=parent,
)
@property
def start_coordinate(self) -> ResidueCoordinate:
"""Get the start target coordinate for the line."""
return self.target[0]
@property
def end_coordinate(self) -> ResidueCoordinate:
"""Get the end target coordinate for the line."""
return self.target[1]
@property
def default_style(self) -> LineAnnotationStyle:
"""Provides the default style for LineAnnotation elements."""
return LineAnnotationStyle()
def get_coordinates(self, resolver: CoordinateResolver) -> np.ndarray:
"""Calculate the start and end coordinates for the line annotation.
Uses the CoordinateResolver to find the rendered coordinates of the two target residues.
Args:
resolver: The CoordinateResolver instance for the scene.
Returns:
A NumPy array of shape [2, 3] containing the (X, Y, Z) coordinates
of the start and end points.
Raises:
CoordinateCalculationError: If coordinates cannot be resolved.
TargetResidueNotFoundError: If a target residue is not found.
"""
start_res = self.start_coordinate
end_res = self.end_coordinate
start_point = resolver.resolve(start_res)
end_point = resolver.resolve(end_res)
# Return as a [2, 3] array
return np.array([start_point, end_point])
|
default_style
property
Provides the default style for LineAnnotation elements.
end_coordinate
property
Get the end target coordinate for the line.
start_coordinate
property
Get the start target coordinate for the line.
__init__(id, start_coordinate, end_coordinate, style=None, label=None, parent=None)
Initializes a LineAnnotation.
Parameters: |
-
id
(str )
–
A unique identifier for this annotation element.
-
target_coordinates
–
A list containing exactly two ResidueCoordinates
defining the start and end points of the line.
-
style
(Optional[LineAnnotationStyle] , default:
None
)
–
The specific style instance for this line annotation.
-
label
(Optional[str] , default:
None
)
–
The label for the annotation.
-
parent
(Optional[SceneGroupType] , default:
None
)
–
The parent SceneGroup in the scene graph, if any.
|
Raises: |
-
ValueError
–
If target_coordinates does not contain exactly two elements.
-
TypeError
–
If elements in target_coordinates are not ResidueCoordinate instances.
|
Source code in src/flatprot/scene/annotation/line.py
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96 | def __init__(
self,
id: str,
start_coordinate: ResidueCoordinate,
end_coordinate: ResidueCoordinate,
style: Optional[LineAnnotationStyle] = None,
label: Optional[str] = None,
parent: Optional[SceneGroupType] = None,
):
"""Initializes a LineAnnotation.
Args:
id: A unique identifier for this annotation element.
target_coordinates: A list containing exactly two ResidueCoordinates
defining the start and end points of the line.
style: The specific style instance for this line annotation.
label: The label for the annotation.
parent: The parent SceneGroup in the scene graph, if any.
Raises:
ValueError: If `target_coordinates` does not contain exactly two elements.
TypeError: If elements in `target_coordinates` are not ResidueCoordinate instances.
"""
if not isinstance(start_coordinate, ResidueCoordinate) or not isinstance(
end_coordinate, ResidueCoordinate
):
raise ValueError(
"LineAnnotation must be initialized with two ResidueCoordinate instances."
)
# Call superclass init
super().__init__(
id=id,
target=[start_coordinate, end_coordinate],
style=style,
label=label,
parent=parent,
)
|
get_coordinates(resolver)
Calculate the start and end coordinates for the line annotation.
Uses the CoordinateResolver to find the rendered coordinates of the two target residues.
Returns: |
-
ndarray
–
A NumPy array of shape [2, 3] containing the (X, Y, Z) coordinates
-
ndarray
–
of the start and end points.
|
Source code in src/flatprot/scene/annotation/line.py
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136 | def get_coordinates(self, resolver: CoordinateResolver) -> np.ndarray:
"""Calculate the start and end coordinates for the line annotation.
Uses the CoordinateResolver to find the rendered coordinates of the two target residues.
Args:
resolver: The CoordinateResolver instance for the scene.
Returns:
A NumPy array of shape [2, 3] containing the (X, Y, Z) coordinates
of the start and end points.
Raises:
CoordinateCalculationError: If coordinates cannot be resolved.
TargetResidueNotFoundError: If a target residue is not found.
"""
start_res = self.start_coordinate
end_res = self.end_coordinate
start_point = resolver.resolve(start_res)
end_point = resolver.resolve(end_res)
# Return as a [2, 3] array
return np.array([start_point, end_point])
|
options:
show_root_heading: true
Bases: BaseAnnotationStyle
Style properties specific to LineAnnotation elements.
Source code in src/flatprot/scene/annotation/line.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52 | class LineAnnotationStyle(BaseAnnotationStyle):
"""Style properties specific to LineAnnotation elements."""
stroke_width: float = Field(
default=1.0, ge=0, description="Width of the annotation line."
)
line_style: Tuple[float, ...] = Field(
default=(5, 5),
description="Dash pattern for the line (e.g., (5, 5) for dashed). Empty tuple means solid.",
)
connector_color: Color = Field(
default=Color("#000000"),
description="Color of the connector circles at the start and end of the line.",
)
line_color: Color = Field(
default=Color("#000000"),
description="Color of the line.",
)
arrowhead_start: bool = Field(
default=False,
description="Whether to draw an arrowhead at the start of the line.",
)
arrowhead_end: bool = Field(
default=False,
description="Whether to draw an arrowhead at the end of the line.",
)
connector_radius: float = Field(
default=2.0,
ge=0,
description="Radius of the connector circles at the start and end of the line.",
)
|
options:
show_root_heading: true
Bases: BaseAnnotationElement[AreaAnnotationStyle]
Represents an annotation highlighting an area encompassing specific residues or ranges.
Source code in src/flatprot/scene/annotation/area.py
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234 | class AreaAnnotation(BaseAnnotationElement[AreaAnnotationStyle]):
"""Represents an annotation highlighting an area encompassing specific residues or ranges."""
def __init__(
self,
id: str,
style: Optional[AreaAnnotationStyle] = None,
label: Optional[str] = None,
residue_range_set: Optional[ResidueRangeSet] = None,
parent: Optional[SceneGroupType] = None,
):
"""Initializes an AreaAnnotation.
Exactly one of `residue_range_set` or `target_coordinates` must be provided
to define the residues encompassed by the area.
Args:
id: A unique identifier for this annotation element.
style: The specific style instance for this area annotation.
label: Optional text label for the annotation.
residue_range_set: The set of residue ranges this annotation targets.
parent: The parent SceneGroup in the scene graph, if any.
Raises:
ValueError: If neither or both targeting arguments are provided.
"""
# Metadata argument removed, using label directly
super().__init__(
id=id,
target=residue_range_set,
style=style,
label=label,
parent=parent,
)
self._cached_outline_coords: Optional[np.ndarray] = None
@property
def default_style(self) -> AreaAnnotationStyle:
"""Provides the default style for AreaAnnotation elements."""
return AreaAnnotationStyle()
def get_coordinates(self, resolver: CoordinateResolver) -> np.ndarray:
"""Calculate the padded convex hull outline coordinates for the area annotation.
Fetches coordinates for all residues defined in the residue_range_set
using the CoordinateResolver. Calculates the convex hull if at least 3
points are found.
Args:
resolver: The CoordinateResolver instance for the scene.
Returns:
A NumPy array of 2D + Depth coordinates (shape [N, 3]) representing
the padded convex hull outline of the area (X, Y, AvgDepth).
Raises:
CoordinateCalculationError: If fewer than 3 valid coordinates are found
for the specified residue range set, or if
hull/padding calculation fails.
"""
logger.debug(f"Calculating area coordinates for '{self.id}' using resolver")
if self.target is None:
raise ValueError(f"AreaAnnotation '{self.id}' has no target defined.")
# 1. Collect all available target 3D coordinates using the resolver
target_coords_3d_list: List[np.ndarray] = []
for res_coord in self.target:
try:
point = resolver.resolve(res_coord)
target_coords_3d_list.append(point)
except (CoordinateCalculationError, TargetResidueNotFoundError) as e:
logger.warning(
f"Could not resolve coordinate for {res_coord} in AreaAnnotation '{self.id}': {e}. Skipping point."
)
# Let unexpected errors propagate
if len(target_coords_3d_list) < 3:
raise CoordinateCalculationError(
f"Need at least 3 resolvable points to calculate area for annotation '{self.id}', found {len(target_coords_3d_list)} within its range set."
)
# Convert list to numpy array for calculations
target_coords_3d = np.array(target_coords_3d_list)
target_coords_2d = target_coords_3d[:, :2] # Use only XY for shape calculation
avg_depth = float(np.mean(target_coords_3d[:, 2])) # Calculate average depth
# 2. Compute the convex hull using Andrew's monotone chain algorithm
hull_points_2d = _convex_hull(target_coords_2d)
# 3. Apply padding by offsetting the vertices of the convex hull
padding = self.style.padding
if padding > 0 and len(hull_points_2d) > 0: # Add check for non-empty hull
padded_points_2d = _apply_padding(hull_points_2d, padding)
else:
padded_points_2d = hull_points_2d
# 4. Combine XY with the calculated average depth
if len(padded_points_2d) == 0:
# This could happen if input points were collinear/identical and hull failed
raise CoordinateCalculationError(
f"Could not compute valid outline for AreaAnnotation '{self.id}' after padding. This is a calculation issue, please check the residue range set."
)
num_outline_points = len(padded_points_2d)
depth_column = np.full((num_outline_points, 1), avg_depth)
outline_coords_3d = np.hstack((padded_points_2d, depth_column))
# self._cached_outline_coords = outline_coords_3d # Removed caching
logger.debug(f"Successfully calculated area coordinates for '{self.id}'")
return outline_coords_3d
|
default_style
property
Provides the default style for AreaAnnotation elements.
__init__(id, style=None, label=None, residue_range_set=None, parent=None)
Initializes an AreaAnnotation.
Exactly one of residue_range_set
or target_coordinates
must be provided
to define the residues encompassed by the area.
Parameters: |
-
id
(str )
–
A unique identifier for this annotation element.
-
style
(Optional[AreaAnnotationStyle] , default:
None
)
–
The specific style instance for this area annotation.
-
label
(Optional[str] , default:
None
)
–
Optional text label for the annotation.
-
residue_range_set
(Optional[ResidueRangeSet] , default:
None
)
–
The set of residue ranges this annotation targets.
-
parent
(Optional[SceneGroupType] , default:
None
)
–
The parent SceneGroup in the scene graph, if any.
|
Raises: |
-
ValueError
–
If neither or both targeting arguments are provided.
|
Source code in src/flatprot/scene/annotation/area.py
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157 | def __init__(
self,
id: str,
style: Optional[AreaAnnotationStyle] = None,
label: Optional[str] = None,
residue_range_set: Optional[ResidueRangeSet] = None,
parent: Optional[SceneGroupType] = None,
):
"""Initializes an AreaAnnotation.
Exactly one of `residue_range_set` or `target_coordinates` must be provided
to define the residues encompassed by the area.
Args:
id: A unique identifier for this annotation element.
style: The specific style instance for this area annotation.
label: Optional text label for the annotation.
residue_range_set: The set of residue ranges this annotation targets.
parent: The parent SceneGroup in the scene graph, if any.
Raises:
ValueError: If neither or both targeting arguments are provided.
"""
# Metadata argument removed, using label directly
super().__init__(
id=id,
target=residue_range_set,
style=style,
label=label,
parent=parent,
)
self._cached_outline_coords: Optional[np.ndarray] = None
|
get_coordinates(resolver)
Calculate the padded convex hull outline coordinates for the area annotation.
Fetches coordinates for all residues defined in the residue_range_set
using the CoordinateResolver. Calculates the convex hull if at least 3
points are found.
Returns: |
-
ndarray
–
A NumPy array of 2D + Depth coordinates (shape [N, 3]) representing
-
ndarray
–
the padded convex hull outline of the area (X, Y, AvgDepth).
|
Raises: |
-
CoordinateCalculationError
–
If fewer than 3 valid coordinates are found
for the specified residue range set, or if
hull/padding calculation fails.
|
Source code in src/flatprot/scene/annotation/area.py
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234 | def get_coordinates(self, resolver: CoordinateResolver) -> np.ndarray:
"""Calculate the padded convex hull outline coordinates for the area annotation.
Fetches coordinates for all residues defined in the residue_range_set
using the CoordinateResolver. Calculates the convex hull if at least 3
points are found.
Args:
resolver: The CoordinateResolver instance for the scene.
Returns:
A NumPy array of 2D + Depth coordinates (shape [N, 3]) representing
the padded convex hull outline of the area (X, Y, AvgDepth).
Raises:
CoordinateCalculationError: If fewer than 3 valid coordinates are found
for the specified residue range set, or if
hull/padding calculation fails.
"""
logger.debug(f"Calculating area coordinates for '{self.id}' using resolver")
if self.target is None:
raise ValueError(f"AreaAnnotation '{self.id}' has no target defined.")
# 1. Collect all available target 3D coordinates using the resolver
target_coords_3d_list: List[np.ndarray] = []
for res_coord in self.target:
try:
point = resolver.resolve(res_coord)
target_coords_3d_list.append(point)
except (CoordinateCalculationError, TargetResidueNotFoundError) as e:
logger.warning(
f"Could not resolve coordinate for {res_coord} in AreaAnnotation '{self.id}': {e}. Skipping point."
)
# Let unexpected errors propagate
if len(target_coords_3d_list) < 3:
raise CoordinateCalculationError(
f"Need at least 3 resolvable points to calculate area for annotation '{self.id}', found {len(target_coords_3d_list)} within its range set."
)
# Convert list to numpy array for calculations
target_coords_3d = np.array(target_coords_3d_list)
target_coords_2d = target_coords_3d[:, :2] # Use only XY for shape calculation
avg_depth = float(np.mean(target_coords_3d[:, 2])) # Calculate average depth
# 2. Compute the convex hull using Andrew's monotone chain algorithm
hull_points_2d = _convex_hull(target_coords_2d)
# 3. Apply padding by offsetting the vertices of the convex hull
padding = self.style.padding
if padding > 0 and len(hull_points_2d) > 0: # Add check for non-empty hull
padded_points_2d = _apply_padding(hull_points_2d, padding)
else:
padded_points_2d = hull_points_2d
# 4. Combine XY with the calculated average depth
if len(padded_points_2d) == 0:
# This could happen if input points were collinear/identical and hull failed
raise CoordinateCalculationError(
f"Could not compute valid outline for AreaAnnotation '{self.id}' after padding. This is a calculation issue, please check the residue range set."
)
num_outline_points = len(padded_points_2d)
depth_column = np.full((num_outline_points, 1), avg_depth)
outline_coords_3d = np.hstack((padded_points_2d, depth_column))
# self._cached_outline_coords = outline_coords_3d # Removed caching
logger.debug(f"Successfully calculated area coordinates for '{self.id}'")
return outline_coords_3d
|
options:
show_root_heading: true
Bases: BaseAnnotationStyle
Style properties specific to AreaAnnotation elements.
Source code in src/flatprot/scene/annotation/area.py
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119 | class AreaAnnotationStyle(BaseAnnotationStyle):
"""Style properties specific to AreaAnnotation elements."""
fill_color: Optional[Color] = Field(
default=Color((0, 0, 0, 0)),
description="Optional fill color (hex string). If None, uses 'color' with reduced opacity.",
)
fill_opacity: float = Field(
default=0.3, ge=0.0, le=1.0, description="Opacity for the fill color."
)
stroke_width: float = Field(
default=1.0, ge=0, description="Width of the area outline stroke."
)
line_style: Tuple[float, ...] = Field(
default=(),
description="Dash pattern for the outline (e.g., (5, 5) for dashed). Empty tuple means solid.",
)
padding: float = Field(
default=20.0,
ge=0,
description="Padding pixels added outside the convex hull.",
)
interpolation_points: int = Field(
default=3,
ge=3,
description="Number of points to generate along the hull outline before smoothing.",
)
smoothing_window: int = Field(
default=1,
ge=1,
description="Window size for rolling average smoothing (odd number recommended).",
)
|
options:
show_root_heading: true
Coordinate Resolver
Handles the mapping between residue identifiers and scene coordinates.
Resolves ResidueCoordinates to their final rendered coordinates.
This class iterates through relevant scene elements to find the one
covering the target residue and asks that element for the coordinate
in its specific rendered space.
Source code in src/flatprot/scene/resolver.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137 | class CoordinateResolver:
"""
Resolves ResidueCoordinates to their final rendered coordinates.
This class iterates through relevant scene elements to find the one
covering the target residue and asks that element for the coordinate
in its specific rendered space.
"""
def __init__(
self, structure: Structure, element_registry: Dict[str, BaseSceneElement]
):
"""
Initializes the CoordinateResolver.
Args:
structure: The core Structure object.
element_registry: The Scene's dictionary mapping element IDs to elements.
"""
self._structure = structure
# Filter the registry to only contain structure elements for efficiency
self._structure_elements = [
element
for element in element_registry.values()
if isinstance(element, BaseStructureSceneElement)
]
def resolve(self, target_residue: ResidueCoordinate) -> np.ndarray:
"""
Finds the covering structure element and gets the rendered coordinate.
Args:
target_residue: The ResidueCoordinate to resolve.
Returns:
A NumPy array [3,] with the resolved (X, Y, Z) coordinate.
Raises:
TargetResidueNotFoundError: If the residue is not found within any
covering structure element's range.
CoordinateCalculationError: If the covering element exists but fails
to calculate the specific coordinate, or
if no covering element is found.
"""
covering_element: Optional[BaseStructureSceneElement] = None
for element in self._structure_elements:
# Check if the element's range set exists and contains the target
if (
element.residue_range_set
and target_residue in element.residue_range_set
):
covering_element = element
break # Use the first one found
if covering_element is None:
logger.warning(
f"No structure element found covering target residue {target_residue}."
)
# Raise specific error indicating no element coverage
raise CoordinateCalculationError(
f"Target residue {target_residue} is not covered by any structure element in the scene."
)
# Ask the covering element for the coordinate
try:
resolved_coord = covering_element.get_coordinate_at_residue(
target_residue, self._structure
)
if resolved_coord is None:
# Element covered the range but couldn't resolve the specific point
logger.warning(
f"Element '{covering_element.id}' could not provide coordinate for {target_residue}."
)
raise CoordinateCalculationError(
f"Element '{covering_element.id}' failed to resolve coordinate for {target_residue}."
)
# Validate shape
if not isinstance(resolved_coord, np.ndarray) or resolved_coord.shape != (
3,
):
logger.error(
f"Element '{covering_element.id}' returned invalid coordinate shape for {target_residue}: {type(resolved_coord)} shape {getattr(resolved_coord, 'shape', 'N/A')}"
)
raise CoordinateCalculationError(
f"Element '{covering_element.id}' returned invalid coordinate data for {target_residue}."
)
return resolved_coord
except TargetResidueNotFoundError as e:
# This can happen if the element's internal lookup fails
logger.warning(
f"Element '{covering_element.id}' could not find {target_residue} internally: {e}"
)
raise # Re-raise the specific error
except CoordinateCalculationError as e:
logger.error(
f"Coordinate calculation error within element '{covering_element.id}' for {target_residue}: {e}",
exc_info=True,
)
raise # Re-raise calculation errors from the element
except Exception as e:
# Catch unexpected errors from the element's method
logger.error(
f"Unexpected error in get_coordinate_at_residue for element '{covering_element.id}' and {target_residue}: {e}",
exc_info=True,
)
raise CoordinateCalculationError(
f"Unexpected error resolving coordinate for {target_residue} via element '{covering_element.id}'."
) from e
|
__init__(structure, element_registry)
Initializes the CoordinateResolver.
Parameters: |
-
structure
(Structure )
–
The core Structure object.
-
element_registry
(Dict[str, BaseSceneElement] )
–
The Scene's dictionary mapping element IDs to elements.
|
Source code in src/flatprot/scene/resolver.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49 | def __init__(
self, structure: Structure, element_registry: Dict[str, BaseSceneElement]
):
"""
Initializes the CoordinateResolver.
Args:
structure: The core Structure object.
element_registry: The Scene's dictionary mapping element IDs to elements.
"""
self._structure = structure
# Filter the registry to only contain structure elements for efficiency
self._structure_elements = [
element
for element in element_registry.values()
if isinstance(element, BaseStructureSceneElement)
]
|
resolve(target_residue)
Finds the covering structure element and gets the rendered coordinate.
Returns: |
-
ndarray
–
A NumPy array [3,] with the resolved (X, Y, Z) coordinate.
|
Source code in src/flatprot/scene/resolver.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137 | def resolve(self, target_residue: ResidueCoordinate) -> np.ndarray:
"""
Finds the covering structure element and gets the rendered coordinate.
Args:
target_residue: The ResidueCoordinate to resolve.
Returns:
A NumPy array [3,] with the resolved (X, Y, Z) coordinate.
Raises:
TargetResidueNotFoundError: If the residue is not found within any
covering structure element's range.
CoordinateCalculationError: If the covering element exists but fails
to calculate the specific coordinate, or
if no covering element is found.
"""
covering_element: Optional[BaseStructureSceneElement] = None
for element in self._structure_elements:
# Check if the element's range set exists and contains the target
if (
element.residue_range_set
and target_residue in element.residue_range_set
):
covering_element = element
break # Use the first one found
if covering_element is None:
logger.warning(
f"No structure element found covering target residue {target_residue}."
)
# Raise specific error indicating no element coverage
raise CoordinateCalculationError(
f"Target residue {target_residue} is not covered by any structure element in the scene."
)
# Ask the covering element for the coordinate
try:
resolved_coord = covering_element.get_coordinate_at_residue(
target_residue, self._structure
)
if resolved_coord is None:
# Element covered the range but couldn't resolve the specific point
logger.warning(
f"Element '{covering_element.id}' could not provide coordinate for {target_residue}."
)
raise CoordinateCalculationError(
f"Element '{covering_element.id}' failed to resolve coordinate for {target_residue}."
)
# Validate shape
if not isinstance(resolved_coord, np.ndarray) or resolved_coord.shape != (
3,
):
logger.error(
f"Element '{covering_element.id}' returned invalid coordinate shape for {target_residue}: {type(resolved_coord)} shape {getattr(resolved_coord, 'shape', 'N/A')}"
)
raise CoordinateCalculationError(
f"Element '{covering_element.id}' returned invalid coordinate data for {target_residue}."
)
return resolved_coord
except TargetResidueNotFoundError as e:
# This can happen if the element's internal lookup fails
logger.warning(
f"Element '{covering_element.id}' could not find {target_residue} internally: {e}"
)
raise # Re-raise the specific error
except CoordinateCalculationError as e:
logger.error(
f"Coordinate calculation error within element '{covering_element.id}' for {target_residue}: {e}",
exc_info=True,
)
raise # Re-raise calculation errors from the element
except Exception as e:
# Catch unexpected errors from the element's method
logger.error(
f"Unexpected error in get_coordinate_at_residue for element '{covering_element.id}' and {target_residue}: {e}",
exc_info=True,
)
raise CoordinateCalculationError(
f"Unexpected error resolving coordinate for {target_residue} via element '{covering_element.id}'."
) from e
|
options:
show_root_heading: true
members_order: source
Scene Utilities
Helper functions for creating and modifying scenes.
Creates a Scene object populated with elements derived from a Structure.
Iterates through chains and secondary structure elements within the provided
Structure object, creating corresponding SceneGroup and structure-specific
SceneElements (Helix, Sheet, Coil). Elements within each chain group are
sorted by their mean depth based on the pre-calculated coordinates in the
Structure object.
Parameters: |
-
structure
(Structure )
–
The core Structure object containing chain, secondary structure,
and pre-projected coordinate data (X, Y, Depth).
-
default_styles
(Optional[Dict[str, Union[BaseStructureStyle, ConnectionStyle]]] , default:
None
)
–
An optional dictionary mapping lowercase element type names
('helix', 'sheet', 'coil', 'connection') to specific style
instances to be used as defaults. If not provided or a type
is missing, the element's own default style will be used.
|
Returns: |
-
Scene
–
A Scene object representing the structure.
|
Source code in src/flatprot/utils/scene_utils.py
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183 | def create_scene_from_structure(
structure: Structure,
default_styles: Optional[
Dict[str, Union[BaseStructureStyle, ConnectionStyle]]
] = None,
) -> Scene:
"""Creates a Scene object populated with elements derived from a Structure.
Iterates through chains and secondary structure elements within the provided
Structure object, creating corresponding SceneGroup and structure-specific
SceneElements (Helix, Sheet, Coil). Elements within each chain group are
sorted by their mean depth based on the pre-calculated coordinates in the
Structure object.
Args:
structure: The core Structure object containing chain, secondary structure,
and pre-projected coordinate data (X, Y, Depth).
default_styles: An optional dictionary mapping lowercase element type names
('helix', 'sheet', 'coil', 'connection') to specific style
instances to be used as defaults. If not provided or a type
is missing, the element's own default style will be used.
Returns:
A Scene object representing the structure.
Raises:
SceneCreationError: If Structure has no coordinates or other critical errors occur.
CoordinateCalculationError: If depth calculation fails for an element.
"""
if structure.coordinates is None or len(structure.coordinates) == 0:
raise SceneCreationError(f"Structure '{structure.id}' has no coordinates.")
scene = Scene(structure=structure)
styles = default_styles or {}
for chain_id, chain in structure:
chain_group = SceneGroup(id=f"{structure.id}_{chain_id}")
scene.add_element(chain_group)
logger.debug(f"\t>Adding chain group {chain_group.id} to scene")
elements_with_depth = []
chain_elements_in_order: List[BaseStructureSceneElement] = []
for ss_element in chain.secondary_structure:
ss_type = ss_element.secondary_structure_type
element_info = STRUCTURE_ELEMENT_MAP.get(ss_type)
if not element_info:
logger.warning(f"Unsupported secondary structure type: {ss_type.value}")
continue
ElementClass, DefaultStyleClass = element_info
# Ensure ss_element start/end are valid ints
start_idx = int(ss_element.start)
end_idx = int(ss_element.end)
ss_range_set = ResidueRangeSet(
[ResidueRange(chain_id=chain_id, start=start_idx, end=end_idx)]
)
# Determine the style: Use provided default or element's default
element_type_key = ss_type.name.lower()
style_instance = styles.get(element_type_key, None)
try:
viz_element = ElementClass(
residue_range_set=ss_range_set,
style=style_instance, # Pass the specific instance or None
)
# Calculate depth based on *pre-projected* coords in structure
depth = viz_element.get_depth(structure)
if depth is None:
# Raise or warn if depth calculation fails
raise CoordinateCalculationError(
f"Could not calculate depth for element {viz_element.id}"
)
elements_with_depth.append((viz_element, depth))
chain_elements_in_order.append(
viz_element
) # <-- Add element to ordered list
except CoordinateCalculationError as e:
# Log or handle coordinate/depth errors
# For now, re-raise to indicate a problem
raise SceneCreationError(
f"Error processing element {ss_type.value} {ss_range_set} in chain {chain_id}: {e}"
) from e
except Exception as e:
# Catch unexpected errors during element creation
raise SceneCreationError(
f"Unexpected error creating element {ss_type.value} {ss_range_set} in chain {chain_id}: {e}"
) from e
# Sort elements by depth (farthest first)
elements_with_depth.sort(key=lambda x: x[1], reverse=True)
# Add Connections between adjacent elements in the original structural order
for i in range(len(chain_elements_in_order) - 1):
element_i = chain_elements_in_order[i]
element_i_plus_1 = chain_elements_in_order[i + 1]
if element_i.is_adjacent_to(element_i_plus_1):
# Get the default connection style if provided
conn_style = styles.get("connection", None)
# Ensure it's a ConnectionStyle or None before passing
if conn_style is not None and not isinstance(
conn_style, ConnectionStyle
):
logger.warning(
f"Invalid type provided for 'connection' style. Expected ConnectionStyle, got {type(conn_style)}. Using default."
)
conn_style = None
conn = Connection(
start_element=element_i,
end_element=element_i_plus_1,
style=conn_style, # Pass the default style
)
logger.debug(
f"\t>Adding connection {conn.id} to chain group {chain_group.id}"
)
scene.add_element(conn, parent_id=chain_group.id)
# Add sorted elements to the chain group
for element, _ in elements_with_depth:
logger.debug(
f"\t>Adding element {element.id} to chain group {chain_group.id}"
)
scene.add_element(element, parent_id=chain_group.id)
return scene
|
options:
show_root_heading: true
Parses annotations from a file and adds them to the scene.
Parameters: |
-
annotations_path
(Path )
–
Path to the TOML annotations file.
-
scene
(Scene )
–
The Scene object to add annotations to.
|
Source code in src/flatprot/utils/scene_utils.py
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234 | def add_annotations_to_scene(annotations_path: Path, scene: Scene) -> None:
"""Parses annotations from a file and adds them to the scene.
Args:
annotations_path: Path to the TOML annotations file.
scene: The Scene object to add annotations to.
Raises:
AnnotationFileNotFoundError: If the annotation file is not found.
MalformedAnnotationError: If the annotation file has invalid content or format.
AnnotationError: For other annotation parsing related errors.
SceneCreationError: If adding an element to the scene fails (e.g., duplicate ID).
"""
try:
# Instantiate the parser with just the file path
parser = AnnotationParser(annotations_path)
# Parse the file to get fully initialized annotation objects
annotation_objects: List[BaseAnnotationElement] = parser.parse()
logger.info(
f"Loaded {len(annotation_objects)} annotations from {annotations_path}"
)
for annotation in annotation_objects:
logger.debug(
f"\t> Adding annotation '{annotation.id}' ({annotation.__class__.__name__}) to scene"
)
try:
scene.add_element(annotation)
except Exception as e:
logger.error(
f"Failed to add annotation '{annotation.id}' to scene: {e}"
)
raise SceneCreationError(
f"Failed to add annotation '{annotation.id}' to scene: {e}"
) from e
except (
AnnotationFileNotFoundError,
MalformedAnnotationError,
AnnotationError,
) as e:
logger.error(f"Failed to parse annotations from {annotations_path}: {e}")
# Re-raise parser errors as they indicate a problem with the input file
raise
except Exception as e:
logger.error(f"An unexpected error occurred while adding annotations: {str(e)}")
# Re-raise unexpected errors
raise
|
options:
show_root_heading: true
Creates a Scene containing only specified domains, each in its own group.
Elements (structure, connections, annotations) are assigned to their respective
domain group. Elements not belonging to any defined domain are discarded.
Domain groups are progressively translated: last domain stays at origin,
earlier domains get negative progressive translations (i×gap_x, i×gap_y) where i is negative.
Parameters: |
-
projected_structure
(Structure )
–
The Structure object with final 2D projected coordinates.
-
domain_definitions
(List[DomainTransformation] )
–
List of DomainTransformation objects defining the domains.
The domain_id attribute is crucial.
-
gap_x
(float , default:
0.0
)
–
Progressive horizontal gap between domains in pixels (last domain at origin).
-
gap_y
(float , default:
0.0
)
–
Progressive vertical gap between domains in pixels (last domain at origin).
-
arrangement
(str , default:
'horizontal'
)
–
How to arrange domain groups (kept for compatibility).
-
default_styles
(Optional[Dict[str, Union[BaseStructureStyle, ConnectionStyle, AreaAnnotationStyle]]] , default:
None
)
–
Optional dictionary mapping element type names to style instances.
-
domain_scop_ids
(Optional[Dict[str, str]] , default:
None
)
–
Optional dictionary mapping domain_id to an annotation string
(e.g., SCOP ID) used for AreaAnnotation labels.
-
domain_alignment_probabilities
(Optional[Dict[str, float]] , default:
None
)
–
Optional dictionary mapping domain_id to alignment
probability (0.0-1.0) for display in annotations.
|
Returns: |
-
Scene
–
A Scene object containing only the specified domain groups, laid out progressively.
|
Raises: |
-
ValueError
–
If structure lacks coordinates, or
domain_definitions have issues (missing IDs when needed, duplicates).
-
TypeError
–
If incompatible style types are provided in default_styles.
|
Source code in src/flatprot/utils/domain_utils.py
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464 | def create_domain_aware_scene(
projected_structure: Structure,
domain_definitions: List[DomainTransformation],
gap_x: float = 0.0,
gap_y: float = 0.0,
arrangement: str = "horizontal",
default_styles: Optional[
Dict[str, Union[BaseStructureStyle, ConnectionStyle, AreaAnnotationStyle]]
] = None,
domain_scop_ids: Optional[Dict[str, str]] = None,
domain_alignment_probabilities: Optional[Dict[str, float]] = None,
) -> Scene:
"""Creates a Scene containing only specified domains, each in its own group.
Elements (structure, connections, annotations) are assigned to their respective
domain group. Elements not belonging to any defined domain are discarded.
Domain groups are progressively translated: last domain stays at origin,
earlier domains get negative progressive translations (i×gap_x, i×gap_y) where i is negative.
Args:
projected_structure: The Structure object with final 2D projected coordinates.
domain_definitions: List of DomainTransformation objects defining the domains.
The domain_id attribute is crucial.
gap_x: Progressive horizontal gap between domains in pixels (last domain at origin).
gap_y: Progressive vertical gap between domains in pixels (last domain at origin).
arrangement: How to arrange domain groups (kept for compatibility).
default_styles: Optional dictionary mapping element type names to style instances.
domain_scop_ids: Optional dictionary mapping domain_id to an annotation string
(e.g., SCOP ID) used for AreaAnnotation labels.
domain_alignment_probabilities: Optional dictionary mapping domain_id to alignment
probability (0.0-1.0) for display in annotations.
Returns:
A Scene object containing only the specified domain groups, laid out progressively.
Raises:
ValueError: If structure lacks coordinates, or
domain_definitions have issues (missing IDs when needed, duplicates).
TypeError: If incompatible style types are provided in default_styles.
"""
if (
projected_structure.coordinates is None
or projected_structure.coordinates.size == 0
):
raise ValueError("Input projected_structure has no coordinates.")
if domain_scop_ids and any(d.domain_id is None for d in domain_definitions):
raise ValueError(
"All domain_definitions must have a domain_id if domain_scop_ids is provided."
)
defined_domain_ids = [d.domain_id for d in domain_definitions if d.domain_id]
if len(defined_domain_ids) != len(set(defined_domain_ids)):
raise ValueError("Duplicate domain_ids found in domain_definitions.")
if domain_scop_ids:
scop_keys = set(domain_scop_ids.keys())
if not scop_keys.issubset(set(defined_domain_ids)):
missing = scop_keys - set(defined_domain_ids)
raise ValueError(f"domain_scop_ids keys not in domain_ids: {missing}")
scene = Scene(structure=projected_structure)
styles = default_styles or {}
domain_groups: Dict[str, SceneGroup] = {} # Map domain_id to SceneGroup
domain_ids_in_order: List[str] = [] # Maintain order for fixed layout
domain_tf_lookup: Dict[str, DomainTransformation] = {}
# --- 1. Create Domain Groups Only ---
logger.debug("Creating scene groups for defined domains...")
for domain_tf in domain_definitions:
domain_id = domain_tf.domain_id or str(domain_tf.domain_range)
if domain_id in domain_groups:
logger.warning(f"Skipping duplicate domain ID '{domain_id}'.")
continue
domain_group = SceneGroup(id=domain_id, transforms=GroupTransform())
scene.add_element(domain_group)
domain_groups[domain_id] = domain_group
domain_ids_in_order.append(domain_id)
domain_tf_lookup[domain_id] = domain_tf
logger.debug(f"Created {len(domain_groups)} domain groups.")
if not domain_groups:
logger.warning("No domain groups created. Scene will be empty.")
return scene # Return early if no domains defined/created
# --- 2. Assign Structure Elements ONLY to Domain Groups ---
logger.debug("Assigning structure elements to domain groups...")
element_map: Dict[str, SceneGroup] = {} # Map element ID to its parent group
elements_assigned_count = 0
elements_discarded_count = 0
for _, chain in projected_structure:
for ss_element in chain.secondary_structure:
ss_type = ss_element.secondary_structure_type
element_info = STRUCTURE_ELEMENT_MAP.get(ss_type)
if not element_info:
elements_discarded_count += 1
continue # Skip unsupported types
ElementClass, _ = element_info
ss_range_set = ResidueRangeSet([ss_element])
element_type_key = ss_type.name.lower()
assigned_group: Optional[SceneGroup] = None
# Find the domain this element belongs to
for domain_tf in domain_definitions:
if ss_element in domain_tf.domain_range:
domain_id = domain_tf.domain_id or str(domain_tf.domain_range)
assigned_group = domain_groups.get(domain_id)
break # Assign to first matching domain
# If element belongs to a defined domain, create and add it
if assigned_group:
try:
base_style = styles.get(element_type_key)
# Type check base style
if base_style is not None and not isinstance(
base_style, BaseStructureStyle
):
logger.warning(
f"Invalid style type for '{element_type_key}'. Using default."
)
base_style = None
viz_element = ElementClass(
residue_range_set=ss_range_set,
style=base_style, # Pass style or None
)
scene.add_element(viz_element, parent_id=assigned_group.id)
element_map[viz_element.id] = assigned_group
elements_assigned_count += 1
except Exception as e:
logger.error(
f"Error creating/assigning element {ss_element}: {e}",
exc_info=True,
)
elements_discarded_count += 1
else:
# Element does not belong to any defined domain, discard it
elements_discarded_count += 1
logger.debug(
f"Assigned {elements_assigned_count} elements to domain groups. Discarded {elements_discarded_count}."
)
# --- 3. Add Connections ONLY Within the Same Domain Group ---
logger.debug("Adding connections within domain groups...")
all_structure_elements = scene.get_sequential_structure_elements()
connections_added_count = 0
connections_discarded_count = 0
for i in range(len(all_structure_elements) - 1):
element_i = all_structure_elements[i]
element_i_plus_1 = all_structure_elements[i + 1]
# Check adjacency first
if not element_i.is_adjacent_to(element_i_plus_1):
continue
# Find the groups these elements belong to (if they were added)
group_i = element_map.get(element_i.id)
group_i_plus_1 = element_map.get(element_i_plus_1.id)
# Only add connection if both elements exist and are in the SAME group
if group_i is not None and group_i is group_i_plus_1:
try:
base_conn_style = styles.get("connection")
# Type check
if base_conn_style is not None and not isinstance(
base_conn_style, ConnectionStyle
):
logger.warning(
"Invalid type for 'connection' style. Using default."
)
base_conn_style = None
conn = Connection(
start_element=element_i,
end_element=element_i_plus_1,
style=base_conn_style,
)
logger.debug(
f"Adding connection between {element_i.id} and {element_i_plus_1.id} to group {group_i.id}"
)
scene.add_element(conn, parent_id=group_i.id)
connections_added_count += 1
except Exception as e:
logger.error(
f"Failed adding connection between {element_i.id}/{element_i_plus_1.id}: {e}",
exc_info=True,
)
connections_discarded_count += 1 # Count as discarded if creation fails
else:
# Connection spans groups or involves discarded elements
connections_discarded_count += 1
logger.debug(
f"Added {connections_added_count} connections within groups. Discarded {connections_discarded_count}."
)
# --- 4. Add Domain Annotations to Respective Groups ---
if domain_scop_ids:
logger.debug("Adding domain annotations...")
annotations_added_count = 0
base_area_style = styles.get("area_annotation")
logger.debug(f"Base area style: {base_area_style}")
# Type check
if base_area_style is not None and not isinstance(
base_area_style, AreaAnnotationStyle
):
logger.warning("Invalid type for 'area_annotation' style. Using default.")
base_area_style = None
for domain_id, scop_id in domain_scop_ids.items():
group = domain_groups.get(domain_id)
domain_tf = domain_tf_lookup.get(domain_id)
if not group or not domain_tf:
logger.warning(
f"Cannot add annotation for domain '{domain_id}': missing group/definition."
)
continue
try:
target_range_set = ResidueRangeSet([domain_tf.domain_range])
# Create label with SCOP ID and alignment probability
label = scop_id
if (
domain_alignment_probabilities
and domain_id in domain_alignment_probabilities
):
probability = domain_alignment_probabilities[domain_id]
label = (
f"{scop_id}\n({probability:.1%})" # e.g., "3000622\n(85.3%)"
)
annotation = AreaAnnotation(
id=f"{domain_id}_area",
residue_range_set=target_range_set,
style=base_area_style, # Pass style or None
label=label,
)
# Add annotation as child of the specific domain group
scene.add_element(annotation, parent_id=group.id)
annotations_added_count += 1
except Exception as e:
logger.error(
f"Failed adding area annotation for domain {domain_id}: {e}",
exc_info=True,
)
logger.debug(f"Added {annotations_added_count} domain area annotations.")
# --- 5. Apply Progressive Gap Translation to Domain Groups ---
# Apply incremental gap_x and gap_y translation to domains for proper separation
if gap_x != 0.0 or gap_y != 0.0:
logger.debug(
f"Applying progressive gap translation with increments: ({gap_x}, {gap_y})"
)
layout_applied_count = 0
# Apply reverse progressive translation - last domain stays at origin, earlier domains get positive gaps
num_domains = len(domain_ids_in_order)
for i, domain_id in enumerate(domain_ids_in_order):
group = domain_groups.get(domain_id)
if not group:
continue # Should not happen based on checks
# Calculate reverse progressive translation: last domain (i=num_domains-1) stays at (0,0)
# Earlier domains get progressively larger positive translations for visual separation
translate_x = (num_domains - 1 - i) * gap_x
translate_y = (num_domains - 1 - i) * gap_y
if group.transforms is None:
group.transforms = GroupTransform()
group.transforms.translate = (translate_x, translate_y)
logger.debug(
f"Applied progressive translation to group {domain_id}: ({translate_x:.2f}, {translate_y:.2f})"
)
layout_applied_count += 1
logger.debug(
f"Applied progressive gap translations to {layout_applied_count} domain groups."
)
else:
logger.debug("Keeping domains in original positions (gap_x = gap_y = 0.0).")
# Ensure groups have identity transforms but no translation
for domain_id in domain_ids_in_order:
group = domain_groups.get(domain_id)
if group:
if group.transforms is None:
group.transforms = GroupTransform()
group.transforms.translate = (0.0, 0.0) # Keep in original position
return scene
|
options:
show_root_heading: true
Scene Errors
Exceptions specific to scene creation or processing.
CircularDependencyError
Bases: SceneError
, ValueError
Raised when an operation would create a circular parent-child relationship.
Source code in src/flatprot/scene/errors.py
| class CircularDependencyError(SceneError, ValueError): # Inherit ValueError for context
"""Raised when an operation would create a circular parent-child relationship."""
pass
|
DuplicateElementError
Bases: SceneError
Raised when attempting to add an element that already exists.
Source code in src/flatprot/scene/errors.py
| class DuplicateElementError(SceneError):
"""Raised when attempting to add an element that already exists."""
pass
|
ElementNotFoundError
Bases: SceneError
Raised when a scene element is not found, typically by ID.
Source code in src/flatprot/scene/errors.py
| class ElementNotFoundError(SceneError):
"""Raised when a scene element is not found, typically by ID."""
pass
|
ElementTypeError
Bases: SceneError
, TypeError
Raised when an element is not of the expected type (e.g., expecting SceneGroup).
Source code in src/flatprot/scene/errors.py
| class ElementTypeError(SceneError, TypeError): # Inherit TypeError for type context
"""Raised when an element is not of the expected type (e.g., expecting SceneGroup)."""
pass
|
InvalidSceneOperationError
Bases: SceneError
, ValueError
Raised for operations that are invalid given the current element state (e.g., adding already parented element).
Source code in src/flatprot/scene/errors.py
| class InvalidSceneOperationError(SceneError, ValueError): # Inherit ValueError
"""Raised for operations that are invalid given the current element state (e.g., adding already parented element)."""
pass
|
ParentNotFoundError
Bases: ElementNotFoundError
Raised when a specified parent element ID is not found.
Source code in src/flatprot/scene/errors.py
| class ParentNotFoundError(ElementNotFoundError): # Inherits as it's a specific case
"""Raised when a specified parent element ID is not found."""
pass
|
SceneAnnotationError
Bases: SceneError
Error related to scene annotations.
Source code in src/flatprot/scene/errors.py
| class SceneAnnotationError(SceneError):
"""Error related to scene annotations."""
pass
|
SceneCreationError
Bases: SceneError
Raised when creation of a scene fails.
Source code in src/flatprot/scene/errors.py
| class SceneCreationError(SceneError):
"""Raised when creation of a scene fails."""
pass
|
SceneError
Bases: FlatProtError
Base class for all errors in the scene module.
Source code in src/flatprot/scene/errors.py
| class SceneError(FlatProtError):
"""Base class for all errors in the scene module."""
pass
|
SceneGraphInconsistencyError
Bases: SceneError
, RuntimeError
Raised when an internal inconsistency in the scene graph state is detected.
Source code in src/flatprot/scene/errors.py
| class SceneGraphInconsistencyError(SceneError, RuntimeError): # Inherit RuntimeError
"""Raised when an internal inconsistency in the scene graph state is detected."""
pass
|
TargetResidueNotFoundError
Bases: SceneError
Error raised when a target residue is not found in the structure.
Source code in src/flatprot/scene/errors.py
| class TargetResidueNotFoundError(SceneError):
"""Error raised when a target residue is not found in the structure."""
def __init__(self, structure: Structure, residue: ResidueCoordinate):
message = f"Residue {residue} not found in structure {structure}"
super().__init__(message)
|
options:
show_root_heading: true