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:

  1. Container: The Scene object holds a collection of SceneElement objects (like helices, sheets, coils, annotations).
  2. 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.
  3. 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.
  4. 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.
  5. 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
441
442
443
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
445
446
447
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) –

    The SceneElement to add.

  • 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.

Returns:
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.

Parameters:
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.

Parameters:
Returns:
  • ndarray

    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.

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.

Parameters:
Returns:
  • ndarray

    A NumPy array of shape [1, 3] containing the (X, Y, Z) coordinates

  • ndarray

    of the target point.

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
25
26
27
28
29
30
31
32
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.

Parameters:
Returns:
  • ndarray

    A NumPy array of shape [2, 3] containing the (X, Y, Z) coordinates

  • ndarray

    of the start and end points.

Raises:
  • CoordinateCalculationError

    If coordinates cannot be resolved.

  • TargetResidueNotFoundError

    If a target residue is not found.

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.

Parameters:
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.

Parameters:
Returns:
  • ndarray

    A NumPy array [3,] with the resolved (X, Y, Z) coordinate.

Raises:
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.

Raises:
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.

Raises:
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
43
44
45
46
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
25
26
27
28
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
19
20
21
22
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
37
38
39
40
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
55
56
57
58
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
31
32
33
34
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
13
14
15
16
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
61
62
63
64
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
 7
 8
 9
10
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
49
50
51
52
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
67
68
69
70
71
72
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