Source code for abm_shape_collection.extract_shape_modes

from typing import Callable

import numpy as np
import pandas as pd
from sklearn.decomposition import PCA

from abm_shape_collection.construct_mesh_from_points import construct_mesh_from_points
from abm_shape_collection.extract_mesh_projections import extract_mesh_projections


[docs]def extract_shape_modes( pca: PCA, data: pd.DataFrame, components: int, regions: list[str], order: int, delta: float, _construct_mesh_from_points: Callable = construct_mesh_from_points, _extract_mesh_projections: Callable = extract_mesh_projections, ) -> dict: """ Extract shape modes (latent walks in PC space) at the specified intervals. Parameters ---------- pca Fit PCA object. data Sample data, with shape coefficients as columns. components Number of shape coefficients components. regions List of regions. order Order of the spherical harmonics coefficient parametrization. delta Interval for latent walk, bounded by -2 and +2 standard deviations. Returns ------- : Map of regions to lists of shape modes at select points. """ # pylint: disable=too-many-locals # Transform data into shape mode space. columns = data.filter(like="shcoeffs").columns transform = pca.transform(data[columns].values) # Calculate transformed means and standard deviations. means = transform.mean(axis=0) stds = transform.std(axis=0, ddof=1) # Create bins. map_points = np.arange(-2, 2.5, delta) bin_edges = [-np.inf] + [point + delta / 2 for point in map_points[:-1]] + [np.inf] transform_binned = np.digitize(transform / stds, bin_edges) # Initialize output dictionary. shape_modes: dict[str, list] = {} for region in regions: region_shape_modes = [] suffix = f".{region}" if region != "DEFAULT" else "" offsets = calculate_region_offsets(data, region) for component in range(components): point_vector = np.zeros(components) for point in map_points: point_bin = np.digitize(point, bin_edges) point_vector[component] = point vector = means + np.multiply(stds, point_vector) indices = transform_binned[:, component] == point_bin mesh = _construct_mesh_from_points(pca, vector, columns, order, suffix=suffix) if region == "DEFAULT" or not any(indices): offset = None else: offset = ( offsets["x"][indices].mean(), offsets["y"][indices].mean(), offsets["z"][indices].mean(), ) region_shape_modes.append( { "mode": component + 1, "point": point, "projections": _extract_mesh_projections( mesh, extents=False, offset=offset ), } ) shape_modes[region] = region_shape_modes return shape_modes
[docs]def calculate_region_offsets(data: pd.DataFrame, region: str) -> dict: """ Calculate offsets for non-default regions. Parameters ---------- data Centroid location data. region Name of region (skipped if region is DEFAULT). Returns ------- : Map of offsets in the x, y, and z directions. """ if region == "DEFAULT": return {} x_deltas = data[f"CENTER_X.{region}"].to_numpy() - data["CENTER_X"].to_numpy() y_deltas = data[f"CENTER_Y.{region}"].to_numpy() - data["CENTER_Y"].to_numpy() z_deltas = data[f"CENTER_Z.{region}"].to_numpy() - data["CENTER_Z"].to_numpy() angles = data["angle"].to_numpy() * np.pi / 180.0 sin_angles = np.sin(angles) cos_angles = np.cos(angles) return { "x": x_deltas * cos_angles - y_deltas * sin_angles, "y": x_deltas * sin_angles + y_deltas * cos_angles, "z": z_deltas, }