Transformation & Projection API

This section documents functions and classes related to transforming protein structures (e.g., alignment, inertia-based) and projecting them into 2D coordinates for visualization.

Transformation Matrix

Defines the core class for handling 3D transformation matrices.

Represents a transformation matrix.

Source code in src/flatprot/transformation/transformation_matrix.py
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 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
 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
@dataclass
class TransformationMatrix:
    """Represents a transformation matrix."""

    rotation: np.ndarray
    translation: np.ndarray

    def __post_init__(self):
        """Validate matrix shapes."""
        if self.rotation.shape != (3, 3):
            raise ValueError("Rotation matrix must be 3x3")
        if self.translation.shape != (3,):
            raise ValueError("Translation vector must have shape (3,)")

    def before(self, other: "TransformationMatrix") -> "TransformationMatrix":
        """Combine two transformation matrices by first applying self, then other.

        The combined transformation T = T2 ∘ T1 is:
        rotation = R2 @ R1
        translation = R2 @ t1 + t2
        """
        return TransformationMatrix(
            rotation=other.rotation @ self.rotation,
            translation=other.rotation @ self.translation + other.translation,
        )

    def after(self, other: "TransformationMatrix") -> "TransformationMatrix":
        """Combine two transformation matrices by first applying other, then self.

        The combined transformation T = T1 ∘ T2 is:
        rotation = R1 @ R2
        translation = R1 @ t2 + t1
        """
        return TransformationMatrix(
            rotation=self.rotation @ other.rotation,
            translation=self.rotation @ other.translation + self.translation,
        )

    def to_array(self) -> np.ndarray:
        """Convert TransformationMatrix to a single numpy array for storage.

        Returns:
            Array of shape (4, 3) where first 3 rows are rotation matrix
            and last row is translation vector
        """
        return np.vstack([self.rotation, self.translation])

    @classmethod
    def from_array(cls, arr: np.ndarray) -> "TransformationMatrix":
        """Create TransformationMatrix from stored array format.

        Args:
            arr: Array of shape (4, 3) where first 3 rows are rotation matrix
                and last row is translation vector

        Returns:
            TransformationMatrix instance
        """
        if arr.shape != (4, 3):
            raise ValueError(f"Input array must be 4x3, but got {arr.shape}")
        # Original slicing for 4x3 array
        return cls(rotation=arr[0:3, :], translation=arr[3, :])

    @classmethod
    def from_string(cls, s: str) -> "TransformationMatrix":
        """Create TransformationMatrix from string representation (assuming 4x3 layout)."""
        # Assuming the string represents a flattened 4x3 matrix
        arr = np.fromstring(s, sep=" ")  # Adjust separator if needed
        if arr.size != 12:
            raise ValueError("String must represent 12 numbers for a 4x3 matrix")
        arr = arr.reshape(4, 3)
        return cls.from_array(arr)

    def apply(self, coordinates: np.ndarray) -> np.ndarray:
        """
        Apply the transformation matrix using the standard (R @ X) + T convention.

        Args:
            coordinates: Array of shape (N, 3) containing 3D coordinates.

        Returns:
            Array of shape (N, 3) containing transformed coordinates.
        """
        if not isinstance(coordinates, np.ndarray):
            raise TypeError("Input coordinates must be a numpy array.")
        if coordinates.ndim != 2 or coordinates.shape[1] != 3:
            raise ValueError(
                f"Input coordinates must have shape (N, 3), got {coordinates.shape}"
            )
        if coordinates.size == 0:
            return coordinates  # Return empty array if input is empty

        # Standard application: Rotate around origin, then translate
        rotated = (self.rotation @ coordinates.T).T
        transformed = rotated + self.translation
        return transformed

    def __eq__(self, other: "TransformationMatrix") -> bool:
        """Check if two TransformationMatrix instances are equal."""
        return np.allclose(self.rotation, other.rotation) and np.allclose(
            self.translation, other.translation
        )

__eq__(other)

Check if two TransformationMatrix instances are equal.

Source code in src/flatprot/transformation/transformation_matrix.py
106
107
108
109
110
def __eq__(self, other: "TransformationMatrix") -> bool:
    """Check if two TransformationMatrix instances are equal."""
    return np.allclose(self.rotation, other.rotation) and np.allclose(
        self.translation, other.translation
    )

__post_init__()

Validate matrix shapes.

Source code in src/flatprot/transformation/transformation_matrix.py
16
17
18
19
20
21
def __post_init__(self):
    """Validate matrix shapes."""
    if self.rotation.shape != (3, 3):
        raise ValueError("Rotation matrix must be 3x3")
    if self.translation.shape != (3,):
        raise ValueError("Translation vector must have shape (3,)")

after(other)

Combine two transformation matrices by first applying other, then self.

The combined transformation T = T1 ∘ T2 is: rotation = R1 @ R2 translation = R1 @ t2 + t1

Source code in src/flatprot/transformation/transformation_matrix.py
35
36
37
38
39
40
41
42
43
44
45
def after(self, other: "TransformationMatrix") -> "TransformationMatrix":
    """Combine two transformation matrices by first applying other, then self.

    The combined transformation T = T1 ∘ T2 is:
    rotation = R1 @ R2
    translation = R1 @ t2 + t1
    """
    return TransformationMatrix(
        rotation=self.rotation @ other.rotation,
        translation=self.rotation @ other.translation + self.translation,
    )

apply(coordinates)

Apply the transformation matrix using the standard (R @ X) + T convention.

Parameters:
  • coordinates (ndarray) –

    Array of shape (N, 3) containing 3D coordinates.

Returns:
  • ndarray

    Array of shape (N, 3) containing transformed coordinates.

Source code in src/flatprot/transformation/transformation_matrix.py
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def apply(self, coordinates: np.ndarray) -> np.ndarray:
    """
    Apply the transformation matrix using the standard (R @ X) + T convention.

    Args:
        coordinates: Array of shape (N, 3) containing 3D coordinates.

    Returns:
        Array of shape (N, 3) containing transformed coordinates.
    """
    if not isinstance(coordinates, np.ndarray):
        raise TypeError("Input coordinates must be a numpy array.")
    if coordinates.ndim != 2 or coordinates.shape[1] != 3:
        raise ValueError(
            f"Input coordinates must have shape (N, 3), got {coordinates.shape}"
        )
    if coordinates.size == 0:
        return coordinates  # Return empty array if input is empty

    # Standard application: Rotate around origin, then translate
    rotated = (self.rotation @ coordinates.T).T
    transformed = rotated + self.translation
    return transformed

before(other)

Combine two transformation matrices by first applying self, then other.

The combined transformation T = T2 ∘ T1 is: rotation = R2 @ R1 translation = R2 @ t1 + t2

Source code in src/flatprot/transformation/transformation_matrix.py
23
24
25
26
27
28
29
30
31
32
33
def before(self, other: "TransformationMatrix") -> "TransformationMatrix":
    """Combine two transformation matrices by first applying self, then other.

    The combined transformation T = T2 ∘ T1 is:
    rotation = R2 @ R1
    translation = R2 @ t1 + t2
    """
    return TransformationMatrix(
        rotation=other.rotation @ self.rotation,
        translation=other.rotation @ self.translation + other.translation,
    )

from_array(arr) classmethod

Create TransformationMatrix from stored array format.

Parameters:
  • arr (ndarray) –

    Array of shape (4, 3) where first 3 rows are rotation matrix and last row is translation vector

Returns:
Source code in src/flatprot/transformation/transformation_matrix.py
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
@classmethod
def from_array(cls, arr: np.ndarray) -> "TransformationMatrix":
    """Create TransformationMatrix from stored array format.

    Args:
        arr: Array of shape (4, 3) where first 3 rows are rotation matrix
            and last row is translation vector

    Returns:
        TransformationMatrix instance
    """
    if arr.shape != (4, 3):
        raise ValueError(f"Input array must be 4x3, but got {arr.shape}")
    # Original slicing for 4x3 array
    return cls(rotation=arr[0:3, :], translation=arr[3, :])

from_string(s) classmethod

Create TransformationMatrix from string representation (assuming 4x3 layout).

Source code in src/flatprot/transformation/transformation_matrix.py
72
73
74
75
76
77
78
79
80
@classmethod
def from_string(cls, s: str) -> "TransformationMatrix":
    """Create TransformationMatrix from string representation (assuming 4x3 layout)."""
    # Assuming the string represents a flattened 4x3 matrix
    arr = np.fromstring(s, sep=" ")  # Adjust separator if needed
    if arr.size != 12:
        raise ValueError("String must represent 12 numbers for a 4x3 matrix")
    arr = arr.reshape(4, 3)
    return cls.from_array(arr)

to_array()

Convert TransformationMatrix to a single numpy array for storage.

Returns:
  • ndarray

    Array of shape (4, 3) where first 3 rows are rotation matrix

  • ndarray

    and last row is translation vector

Source code in src/flatprot/transformation/transformation_matrix.py
47
48
49
50
51
52
53
54
def to_array(self) -> np.ndarray:
    """Convert TransformationMatrix to a single numpy array for storage.

    Returns:
        Array of shape (4, 3) where first 3 rows are rotation matrix
        and last row is translation vector
    """
    return np.vstack([self.rotation, self.translation])

options: show_root_heading: true members_order: source

Inertia-Based Transformation

Classes and functions for calculating and applying transformations based on the principal axes of inertia. These transformations are often used for aligning protein structures in a way that preserves their overall shape and orientation. Commonly these are implemented in orientation features of molecular visualization software.

Calculate transformation matrix for optimal molecular orientation, returning components compatible with standard (R @ X) + T application.

The transformation aligns the principal axes with the coordinate axes and moves the center of mass/geometry C to the origin. This function calculates R_inertia and C, then returns R_standard = R_inertia and T_standard = -(R_inertia @ C).

Parameters:
  • coordinates (ndarray) –

    Nx3 array of atomic coordinates

  • weights (ndarray) –

    N-length array of weights for each coordinate

Returns:
  • TransformationMatrix

    TransformationMatrix with rotation = R_inertia and translation = -(R_inertia @ C)

Source code in src/flatprot/transformation/inertia_transformation.py
18
19
20
21
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def calculate_inertia_transformation_matrix(
    coordinates: np.ndarray, weights: np.ndarray
) -> TransformationMatrix:
    """
    Calculate transformation matrix for optimal molecular orientation,
    returning components compatible with standard (R @ X) + T application.

    The transformation aligns the principal axes with the coordinate axes
    and moves the center of mass/geometry C to the origin.
    This function calculates R_inertia and C, then returns R_standard = R_inertia
    and T_standard = -(R_inertia @ C).

    Args:
        coordinates: Nx3 array of atomic coordinates
        weights: N-length array of weights for each coordinate

    Returns:
        TransformationMatrix with rotation = R_inertia and translation = -(R_inertia @ C)
    """
    # Calculate center (weighted or geometric) C
    if np.allclose(weights, weights[0]):  # All weights equal -> geometric center
        center_C = np.mean(coordinates, axis=0)
    else:  # Weighted center of mass
        total_weight = np.sum(weights)
        if total_weight == 0:
            center_C = np.mean(coordinates, axis=0)  # Fallback for zero weights
        else:
            center_C = (
                np.sum(coordinates * weights[:, np.newaxis], axis=0) / total_weight
            )

    # Center coordinates for inertia tensor calculation
    centered_coords = coordinates - center_C

    # Principal axis method using inertia tensor
    inertia_tensor = np.zeros((3, 3))
    for coord, weight in zip(centered_coords, weights):
        r_squared = np.sum(coord * coord)
        inertia_tensor += weight * (r_squared * np.eye(3) - np.outer(coord, coord))

    _, eigenvectors = np.linalg.eigh(inertia_tensor)
    # Rotation matrix (R_inertia) is composed of the eigenvectors
    rotation_R_inertia = eigenvectors

    # Ensure a right-handed coordinate system
    if np.linalg.det(rotation_R_inertia) < 0:
        rotation_R_inertia[:, 2] *= -1

    # Calculate the standard translation T = -(R_inertia @ C)
    # Ensure C is treated as a column vector for matmul
    center_C_col = center_C.reshape(-1, 1)
    translation_T_standard_col = -(rotation_R_inertia @ center_C_col)
    translation_T_standard = translation_T_standard_col.flatten()  # Back to (3,)

    # Return the matrix with components for standard application
    return TransformationMatrix(
        rotation=rotation_R_inertia, translation=translation_T_standard
    )

options: show_root_heading: true

Bases: BaseTransformationParameters

Parameters for inertia-based projection calculation.

Source code in src/flatprot/transformation/inertia_transformation.py
 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
@dataclass
class InertiaTransformationParameters(BaseTransformationParameters):
    """Parameters for inertia-based projection calculation."""

    residue_weights: dict[ResidueType, float]  # Maps residue type to weight
    use_weights: bool = False

    @classmethod
    def default(cls) -> "InertiaTransformationParameters":
        """Creates default parameters using standard amino acid weights."""
        return cls(
            residue_weights={
                ResidueType.ALA: 89.1,
                ResidueType.ARG: 174.2,
                ResidueType.ASN: 132.1,
                ResidueType.ASP: 133.1,
                ResidueType.CYS: 121.2,
                ResidueType.GLN: 146.2,
                ResidueType.GLU: 147.1,
                ResidueType.GLY: 75.1,
                ResidueType.HIS: 155.2,
                ResidueType.ILE: 131.2,
                ResidueType.LEU: 131.2,
                ResidueType.LYS: 146.2,
                ResidueType.MET: 149.2,
                ResidueType.PHE: 165.2,
                ResidueType.PRO: 115.1,
                ResidueType.SER: 105.1,
                ResidueType.THR: 119.1,
                ResidueType.TRP: 204.2,
                ResidueType.TYR: 181.2,
                ResidueType.VAL: 117.1,
            }
        )

default() classmethod

Creates default parameters using standard amino acid weights.

Source code in src/flatprot/transformation/inertia_transformation.py
 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
@classmethod
def default(cls) -> "InertiaTransformationParameters":
    """Creates default parameters using standard amino acid weights."""
    return cls(
        residue_weights={
            ResidueType.ALA: 89.1,
            ResidueType.ARG: 174.2,
            ResidueType.ASN: 132.1,
            ResidueType.ASP: 133.1,
            ResidueType.CYS: 121.2,
            ResidueType.GLN: 146.2,
            ResidueType.GLU: 147.1,
            ResidueType.GLY: 75.1,
            ResidueType.HIS: 155.2,
            ResidueType.ILE: 131.2,
            ResidueType.LEU: 131.2,
            ResidueType.LYS: 146.2,
            ResidueType.MET: 149.2,
            ResidueType.PHE: 165.2,
            ResidueType.PRO: 115.1,
            ResidueType.SER: 105.1,
            ResidueType.THR: 119.1,
            ResidueType.TRP: 204.2,
            ResidueType.TYR: 181.2,
            ResidueType.VAL: 117.1,
        }
    )

options: show_root_heading: true members_order: source

Bases: BaseTransformation[InertiaTransformationParameters, InertiaTransformationArguments]

Transforms using inertia-based calculation with residue weights.

Source code in src/flatprot/transformation/inertia_transformation.py
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
class InertiaTransformer(
    BaseTransformation[InertiaTransformationParameters, InertiaTransformationArguments]
):
    """Transforms using inertia-based calculation with residue weights."""

    def __init__(self, parameters: InertiaTransformationParameters):
        super().__init__(parameters=parameters)

    def _calculate_transformation_matrix(
        self,
        coordinates: np.ndarray,
        parameters: InertiaTransformationParameters,
    ) -> TransformationMatrix:
        """Calculate transformation matrix for given coordinates."""
        if not self.parameters.use_weights:
            weights = np.ones(len(coordinates))
        else:
            # Map residue types to weights using parameters.residue_weights
            weights = np.array(
                [
                    self.parameters.residue_weights.get(res, 1.0)
                    for res in parameters.residues
                ]
            )

        return calculate_inertia_transformation_matrix(coordinates, weights)

options: show_root_heading: true members_order: source

Matrix-Based Transformation

Classes for applying a pre-defined transformation matrix.

Bases: BaseTransformationParameters

Parameters for matrix-based transformation.

Source code in src/flatprot/transformation/matrix_transformation.py
14
15
16
17
18
@dataclass
class MatrixTransformParameters(BaseTransformationParameters):
    """Parameters for matrix-based transformation."""

    matrix: TransformationMatrix

options: show_root_heading: true members_order: source

Bases: BaseTransformation[MatrixTransformParameters, None]

Projects coordinates using a provided rotation matrix and translation vector.

This projector applies a fixed projection matrix provided at initialization.

Source code in src/flatprot/transformation/matrix_transformation.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class MatrixTransformer(BaseTransformation[MatrixTransformParameters, None]):
    """Projects coordinates using a provided rotation matrix and translation vector.

    This projector applies a fixed projection matrix provided at initialization.
    """

    def __init__(self, parameters: MatrixTransformParameters):
        """Initialize with a fixed transformation matrix.

        Args:
            projection_matrix: The ProjectionMatrix to use for all projections
        """
        super().__init__(parameters)
        self._cached_transformation = parameters.matrix

    def _calculate_transformation_matrix(
        self,
        coordinates: np.ndarray,
    ) -> TransformationMatrix:
        """Return the fixed projection matrix.

        Since this projector uses a fixed projection matrix provided at initialization,
        this method simply returns the cached transformation.
        """
        return self._cached_transformation

__init__(parameters)

Initialize with a fixed transformation matrix.

Parameters:
  • projection_matrix

    The ProjectionMatrix to use for all projections

Source code in src/flatprot/transformation/matrix_transformation.py
27
28
29
30
31
32
33
34
def __init__(self, parameters: MatrixTransformParameters):
    """Initialize with a fixed transformation matrix.

    Args:
        projection_matrix: The ProjectionMatrix to use for all projections
    """
    super().__init__(parameters)
    self._cached_transformation = parameters.matrix

options: show_root_heading: true members_order: source

Structure Transformation Utilities

Helper functions (often used internally by CLI commands) for applying transformations to flatprot.core.Structure objects.

Transforms a Structure using its principal axes of inertia.

Parameters:
  • structure (Structure) –

    The protein structure to transform.

  • custom_config_params (Optional[InertiaTransformationParameters], default: None ) –

    Optional InertiaTransformationParameters to override defaults (e.g., custom residue weights or disabling weights) passed to the InertiaTransformer's initialization.

Returns:
  • Structure

    A new Structure object with transformed coordinates oriented along principal axes.

Raises:
  • TransformationError

    If the structure is unsuitable for inertia calculation (e.g., lacks coordinates or residues), or if the transformation process fails mathematically.

  • ValueError

    If the structure lacks coordinates or residues required for the process.

Source code in src/flatprot/utils/structure_utils.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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
def transform_structure_with_inertia(
    structure: Structure,
    custom_config_params: Optional[InertiaTransformationParameters] = None,
) -> Structure:
    """Transforms a Structure using its principal axes of inertia.

    Args:
        structure: The protein structure to transform.
        custom_config_params: Optional InertiaTransformationParameters to override defaults
                              (e.g., custom residue weights or disabling weights) passed
                              to the InertiaTransformer's initialization.

    Returns:
        A new Structure object with transformed coordinates oriented along principal axes.

    Raises:
        TransformationError: If the structure is unsuitable for inertia calculation
                             (e.g., lacks coordinates or residues), or if the
                             transformation process fails mathematically.
        ValueError: If the structure lacks coordinates or residues required for the process.
    """
    if not hasattr(structure, "coordinates") or structure.coordinates is None:
        raise ValueError("Structure has no coordinates for inertia transformation.")
    if not hasattr(structure, "residues") or not structure.residues:
        raise ValueError("Structure has no residues for inertia transformation.")
    if structure.coordinates.size == 0:
        logger.warning("Structure coordinates are empty, returning original structure.")
        return structure  # Return unchanged structure

    try:
        # Use provided config params or create default ones for the transformer's __init__
        # InertiaTransformationParameters likely holds config like residue_weights dict
        inertia_config_parameters = (
            custom_config_params or InertiaTransformationParameters.default()
        )

        # Instantiate the transformer with its configuration parameters
        transformer = InertiaTransformer(parameters=inertia_config_parameters)

        logger.info("Applying inertia transformation...")

        # Prepare the runtime arguments needed specifically for the transform call
        # InertiaTransformArguments (aliased from InertiaTransformationParameters in inertia.py)
        # likely holds data specific to this call, like the list of residues.
        transform_arguments = InertiaTransformationArguments(
            residues=structure.residues
        )

        # Wrap the transformer's transform method
        def transform_func(coords: np.ndarray) -> np.ndarray:
            # Pass coordinates and the required runtime arguments object
            return transformer.transform(coords, arguments=transform_arguments)

        new_structure = structure.apply_vectorized_transformation(transform_func)
        logger.info("Inertia transformation complete.")
        return new_structure

    except Exception as e:
        logger.error(f"Error during inertia transformation: {str(e)}", exc_info=True)
        # Re-raise as TransformationError if not already one
        if isinstance(e, TransformationError):
            raise
        raise TransformationError(f"Inertia transformation failed: {str(e)}") from e

options: show_root_heading: true

Transforms a Structure using a matrix (loaded from path or identity).

Parameters:
  • structure (Structure) –

    The protein structure to transform.

  • matrix_path (Optional[Path], default: None ) –

    Optional path to a numpy matrix file (.npy) for transformation. If None or loading fails gracefully, the identity matrix is used.

Returns:
  • Structure

    A new Structure object with transformed coordinates.

Raises:
  • TransformationError

    If the transformation process fails unexpectedly (e.g., matrix loading error, issues during transformation math).

  • ValueError

    If the structure lacks coordinates.

Source code in src/flatprot/utils/structure_utils.py
 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
def transform_structure_with_matrix(
    structure: Structure, matrix_path: Optional[Path] = None
) -> Structure:
    """Transforms a Structure using a matrix (loaded from path or identity).

    Args:
        structure: The protein structure to transform.
        matrix_path: Optional path to a numpy matrix file (.npy) for transformation.
                     If None or loading fails gracefully, the identity matrix is used.

    Returns:
        A new Structure object with transformed coordinates.

    Raises:
        TransformationError: If the transformation process fails unexpectedly (e.g.,
                             matrix loading error, issues during transformation math).
        ValueError: If the structure lacks coordinates.
    """
    if not hasattr(structure, "coordinates") or structure.coordinates is None:
        raise ValueError("Structure has no coordinates to transform.")
    if structure.coordinates.size == 0:
        logger.warning("Structure coordinates are empty, returning original structure.")
        return structure  # Return unchanged structure if no coords

    try:
        transformation_matrix = _load_transformation_matrix(matrix_path)
        # Correctly instantiate MatrixTransformer using its parameters class
        transformer_params = MatrixTransformParameters(matrix=transformation_matrix)
        transformer = MatrixTransformer(parameters=transformer_params)

        logger.info(
            f"Applying matrix transformation (source: {matrix_path or 'Identity'})..."
        )

        new_structure = structure.apply_vectorized_transformation(
            lambda coords: transformer.transform(coords, arguments=None)
        )
        logger.info("Matrix transformation complete.")
        return new_structure

    except Exception as e:
        # Catch potential errors from apply_vectorized_transformation or transform_func
        logger.error(f"Error during matrix transformation: {str(e)}", exc_info=True)
        # Re-raise as TransformationError if not already (e.g. ValueError from shape mismatch)
        if isinstance(e, TransformationError):
            raise
        raise TransformationError(f"Matrix transformation failed: {str(e)}") from e

options: show_root_heading: true

Orthographic Projection

Function for projecting a transformed 3D structure onto a 2D canvas.

Projects the coordinates of a Structure orthographically, returning a new Structure.

Assumes the input structure's coordinates are already appropriately transformed (e.g., centered and oriented via inertia or matrix transformation). The coordinates of the returned Structure will be the projected (X, Y, Depth) values.

Parameters:
  • structure (Structure) –

    The Structure object whose coordinates are to be projected. Assumes structure.coordinates holds the transformed 3D points.

  • width (int) –

    The width of the target canvas in pixels.

  • height (int) –

    The height of the target canvas in pixels.

  • padding_x (float, default: 0.05 ) –

    Horizontal padding as a fraction of the width (0 to <0.5).

  • padding_y (float, default: 0.05 ) –

    Vertical padding as a fraction of the height (0 to <0.5).

  • maintain_aspect_ratio (bool, default: True ) –

    Whether to scale uniformly to fit while preserving the structure's shape, or stretch to fill padding box.

  • center_projection (bool, default: True ) –

    Whether to center the projected structure within the canvas.

  • view_direction (Optional[ndarray], default: None ) –

    Optional (3,) numpy array for the view direction (camera looking along -view_direction). Defaults to [0, 0, 1] if None.

  • up_vector (Optional[ndarray], default: None ) –

    Optional (3,) numpy array for the initial up vector. Defaults to [0, 1, 0] if None.

  • disable_scaling (bool, default: False ) –

    If True, disables automatic scaling to fit canvas, useful for overlay comparisons where consistent scales are needed across different structures.

Returns:
  • Structure

    A new Structure object where the coordinates represent the projected data:

  • Structure
    • Column 0: X canvas coordinate (float)
  • Structure
    • Column 1: Y canvas coordinate (float)
  • Structure
    • Column 2: Depth value (float, representing scaled Z for visibility/layering)
Raises:
  • ProjectionError

    If the projection process fails (e.g., mathematical error, invalid parameters).

  • ValueError

    If the structure lacks coordinates or if vector inputs are invalid.

Source code in src/flatprot/utils/structure_utils.py
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
def project_structure_orthographically(
    structure: Structure,
    width: int,
    height: int,
    padding_x: float = 0.05,
    padding_y: float = 0.05,
    maintain_aspect_ratio: bool = True,
    center_projection: bool = True,
    view_direction: Optional[np.ndarray] = None,
    up_vector: Optional[np.ndarray] = None,
    disable_scaling: bool = False,
) -> Structure:
    """Projects the coordinates of a Structure orthographically, returning a new Structure.

    Assumes the input structure's coordinates are already appropriately transformed
    (e.g., centered and oriented via inertia or matrix transformation).
    The coordinates of the returned Structure will be the projected (X, Y, Depth) values.

    Args:
        structure: The Structure object whose coordinates are to be projected.
                   Assumes structure.coordinates holds the transformed 3D points.
        width: The width of the target canvas in pixels.
        height: The height of the target canvas in pixels.
        padding_x: Horizontal padding as a fraction of the width (0 to <0.5).
        padding_y: Vertical padding as a fraction of the height (0 to <0.5).
        maintain_aspect_ratio: Whether to scale uniformly to fit while preserving
                               the structure's shape, or stretch to fill padding box.
        center_projection: Whether to center the projected structure within the canvas.
        view_direction: Optional (3,) numpy array for the view direction (camera looking along -view_direction).
                        Defaults to [0, 0, 1] if None.
        up_vector: Optional (3,) numpy array for the initial up vector.
                   Defaults to [0, 1, 0] if None.
        disable_scaling: If True, disables automatic scaling to fit canvas, useful for overlay
                        comparisons where consistent scales are needed across different structures.

    Returns:
        A new Structure object where the coordinates represent the projected data:
        - Column 0: X canvas coordinate (float)
        - Column 1: Y canvas coordinate (float)
        - Column 2: Depth value (float, representing scaled Z for visibility/layering)

    Raises:
        ProjectionError: If the projection process fails (e.g., mathematical error,
                         invalid parameters).
        ValueError: If the structure lacks coordinates or if vector inputs are invalid.
    """
    if not hasattr(structure, "coordinates") or structure.coordinates is None:
        raise ValueError("Structure has no coordinates to project.")

    coords_to_project = structure.coordinates
    if coords_to_project.size == 0:
        logger.warning("Structure coordinates are empty. Returning original structure.")
        # Return original structure if no coordinates to project
        return structure

    try:
        projector = OrthographicProjection()

        # Construct parameters for the projection
        param_kwargs = {
            "width": width,
            "height": height,
            "padding_x": padding_x,
            "padding_y": padding_y,
            "maintain_aspect_ratio": maintain_aspect_ratio,
            "canvas_alignment": "center" if center_projection else "top_left",
            "disable_scaling": disable_scaling,
        }
        if view_direction is not None:
            param_kwargs["view_direction"] = view_direction
        if up_vector is not None:
            param_kwargs["up_vector"] = up_vector

        projection_params = OrthographicProjectionParameters(**param_kwargs)

        # Define the transformation function that performs the projection
        def projection_func(coords: np.ndarray) -> np.ndarray:
            logger.info("Performing orthographic projection internally...")
            projected = projector.project(coords, projection_params)
            logger.info("Internal projection complete.")
            return projected.astype(np.float32)  # Ensure float32

        # Use apply_vectorized_transformation to create a new structure
        # with the projected coordinates
        logger.info("Applying projection transformation to structure...")
        projected_structure = structure.apply_vectorized_transformation(projection_func)
        logger.info("Projection transformation applied.")

        return projected_structure

    except Exception as e:
        logger.error(f"Error during orthographic projection: {str(e)}", exc_info=True)
        # Re-raise specific errors or wrap in ProjectionError
        if isinstance(e, (ProjectionError, ValueError)):
            raise
        raise ProjectionError(f"Orthographic projection failed: {str(e)}") from e

options: show_root_heading: true

Domain Transformation Utilities

Classes and functions specifically for handling domain-based transformations.

Encapsulates a transformation matrix applied to a specific protein domain.

Attributes:
  • domain_range (ResidueRange) –

    The specific residue range defining the domain.

  • transformation_matrix (TransformationMatrix) –

    The matrix used to transform this domain.

  • domain_id (Optional[str]) –

    An optional identifier for the domain (e.g., 'Domain1', 'N-term').

  • scop_id (Optional[str]) –

    An optional SCOP family identifier from alignment (e.g., '3000114').

  • alignment_probability (Optional[float]) –

    The alignment probability/quality score (0.0-1.0).

Source code in src/flatprot/utils/domain_utils.py
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
@dataclass
class DomainTransformation:
    """
    Encapsulates a transformation matrix applied to a specific protein domain.

    Attributes:
        domain_range: The specific residue range defining the domain.
        transformation_matrix: The matrix used to transform this domain.
        domain_id: An optional identifier for the domain (e.g., 'Domain1', 'N-term').
        scop_id: An optional SCOP family identifier from alignment (e.g., '3000114').
        alignment_probability: The alignment probability/quality score (0.0-1.0).
    """

    domain_range: ResidueRange
    transformation_matrix: TransformationMatrix
    domain_id: Optional[str] = None
    scop_id: Optional[str] = None
    alignment_probability: Optional[float] = None

    def __post_init__(self):
        """Validate inputs."""
        if not isinstance(self.domain_range, ResidueRange):
            raise TypeError("domain_range must be a ResidueRange object.")
        if not isinstance(self.transformation_matrix, TransformationMatrix):
            raise TypeError(
                "transformation_matrix must be a TransformationMatrix object."
            )
        if self.domain_id is not None and not isinstance(self.domain_id, str):
            raise TypeError("domain_id must be a string if provided.")
        if self.scop_id is not None and not isinstance(self.scop_id, str):
            raise TypeError("scop_id must be a string if provided.")

    def __repr__(self) -> str:
        """Provide a concise representation."""
        name = f" '{self.domain_id}'" if self.domain_id else ""
        return f"<DomainTransformation{name} range={self.domain_range}>"

__post_init__()

Validate inputs.

Source code in src/flatprot/utils/domain_utils.py
53
54
55
56
57
58
59
60
61
62
63
64
def __post_init__(self):
    """Validate inputs."""
    if not isinstance(self.domain_range, ResidueRange):
        raise TypeError("domain_range must be a ResidueRange object.")
    if not isinstance(self.transformation_matrix, TransformationMatrix):
        raise TypeError(
            "transformation_matrix must be a TransformationMatrix object."
        )
    if self.domain_id is not None and not isinstance(self.domain_id, str):
        raise TypeError("domain_id must be a string if provided.")
    if self.scop_id is not None and not isinstance(self.scop_id, str):
        raise TypeError("scop_id must be a string if provided.")

__repr__()

Provide a concise representation.

Source code in src/flatprot/utils/domain_utils.py
66
67
68
69
def __repr__(self) -> str:
    """Provide a concise representation."""
    name = f" '{self.domain_id}'" if self.domain_id else ""
    return f"<DomainTransformation{name} range={self.domain_range}>"

options: show_root_heading: true members_order: source

Applies specific transformation matrices to defined domains around their centers.

Each domain is rotated around its geometric center, preserving its position within the global structure while optimizing its orientation for visualization. Uses boolean masks to identify coordinates for each domain.

Creates a new Structure object with transformed 3D coordinates. The original structure remains unchanged.

Parameters:
  • structure (Structure) –

    The original Structure object.

  • domain_transforms (List[DomainTransformation]) –

    An ordered list of DomainTransformation objects. Rotations are applied around each domain's center.

Returns:
  • Structure

    A new Structure object with coordinates transformed domain-specifically.

Raises:
  • TransformationError

    If matrix application fails.

  • ValueError

    If structure lacks coordinates or if coordinates are not 3D.

Source code in src/flatprot/utils/domain_utils.py
 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
def apply_domain_transformations_masked(
    structure: Structure,
    domain_transforms: List[DomainTransformation],
) -> Structure:
    """
    Applies specific transformation matrices to defined domains around their centers.

    Each domain is rotated around its geometric center, preserving its position
    within the global structure while optimizing its orientation for visualization.
    Uses boolean masks to identify coordinates for each domain.

    Creates a new Structure object with transformed 3D coordinates. The original
    structure remains unchanged.

    Args:
        structure: The original Structure object.
        domain_transforms: An ordered list of DomainTransformation objects.
                           Rotations are applied around each domain's center.

    Returns:
        A new Structure object with coordinates transformed domain-specifically.

    Raises:
        TransformationError: If matrix application fails.
        ValueError: If structure lacks coordinates or if coordinates are not 3D.
    """
    if not hasattr(structure, "coordinates") or structure.coordinates is None:
        raise ValueError("Structure has no coordinates to transform.")
    if structure.coordinates.shape[1] != 3:
        raise ValueError(
            f"Expected 3D coordinates, but got shape {structure.coordinates.shape}"
        )

    original_coords = structure.coordinates
    num_atoms = original_coords.shape[0]

    # 1. Create Transformation Masks (one per domain)
    domain_masks: List[np.ndarray] = [
        np.zeros(num_atoms, dtype=bool) for _ in domain_transforms
    ]

    # 2. & 3. Iterate through structure coords and populate masks
    for chain in structure.values():
        for residue in chain:
            for domain_idx, domain_tf in enumerate(domain_transforms):
                if residue in domain_tf.domain_range:
                    if residue.coordinate_index < domain_masks[domain_idx].shape[0]:
                        domain_masks[domain_idx][residue.coordinate_index] = True

    transformed_coords = original_coords.copy()

    for mask, domain_tf in zip(domain_masks, domain_transforms):
        if not np.any(mask):
            logger.warning(
                f"No coordinates found for domain {domain_tf.domain_range}. Skipping transformation."
            )
            continue

        domain_id_str = f" '{domain_tf.domain_id}'" if domain_tf.domain_id else ""
        logger.debug(
            f"Applying centered transformation for domain{domain_id_str} {domain_tf.domain_range} (affecting {np.sum(mask)} coordinates)..."
        )

        try:
            # Calculate domain center
            domain_center = calculate_domain_center(structure, domain_tf.domain_range)

            # Create centered transformation using only rotation from domain_tf
            # This rotates the domain around its center, preserving its position
            rotation = domain_tf.transformation_matrix.rotation
            centered_matrix = create_centered_transformation(rotation, domain_center)

            # Apply transformation to the *original* coordinates selected by the mask
            coords_subset = original_coords[mask, :]
            transformed_subset = centered_matrix.apply(coords_subset)

            if transformed_subset.shape != coords_subset.shape:
                raise TransformationError(
                    f"Transformation resulted in unexpected shape change for domain {domain_tf.domain_range}. "
                    f"Input shape: {coords_subset.shape}, Output shape: {transformed_subset.shape}"
                )

            transformed_coords[mask, :] = transformed_subset

            logger.info(
                f"Applied centered rotation for domain{domain_id_str} {domain_tf.domain_range} "
                f"around center {domain_center}"
            )

        except Exception as e:
            raise TransformationError(
                f"Failed to apply centered transformation for domain {domain_tf.domain_range}: {e}"
            ) from e

    # Create a new structure object with the same topology but new coordinates

    new_structure = structure.with_coordinates(transformed_coords)

    return new_structure

options: show_root_heading: true