Source code for vectorose.triangle_sphere

# Copyright (c) 2024-, Benjamin Rudski, Joseph Deering
#
# This code is licensed under the MIT License. See the `LICENSE` file for
# more details about copying.

"""Triangle-based Sphere Plotting.

This module provides the functions necessary to produce a triangle mesh of
a sphere, with face colours corresponding to the point count in each face.
"""

from typing import List, Optional, Tuple

import numpy as np
import pandas as pd
import pyvista as pv
import trimesh

from .sphere_base import SphereBase
from . import util


[docs] class TriangleSphere(SphereBase): """Representation of a sphere constructed using equal-area triangles. Compute and visualise histograms using a sphere composed of equal-area triangles. """ # Attributes _sphere: trimesh.primitives.Sphere """Sphere mesh used to compute and visualise the histogram.""" _faces: pd.DataFrame """Data frame containing information about the mesh faces.""" @property def orientation_cols(self) -> List[str]: return ["face"]
[docs] def _initial_vector_component_preparation( self, vectors: pd.DataFrame ) -> pd.DataFrame: vectors_array = vectors.loc[:, ["vx", "vy", "vz"]].to_numpy() unit_vectors, magnitudes = util.normalise_vectors(vectors_array) magnitudes = magnitudes[:, None] # Prepare the data to make a new DataFrame vector_data = np.concatenate([unit_vectors, magnitudes], axis=-1) columns = ["ux", "uy", "uz", "magnitude"] # Create a data frame with the unit vectors and magnitudes. unit_vector_data_frame = pd.DataFrame(vector_data, columns=columns) return unit_vector_data_frame
[docs] def _compute_orientation_binning(self, vectors: pd.DataFrame) -> pd.Series: unit_vectors = vectors.loc[:, ["ux", "uy", "uz"]].to_numpy() proximity_query = trimesh.proximity.ProximityQuery(self._sphere) _, _, face_indices = proximity_query.on_surface(unit_vectors) face_series = pd.Series(face_indices, name="face") return face_series
[docs] def to_dataframe(self) -> pd.DataFrame: """Get the data frame representation of the sphere. Notes ----- The produced table contains one row for each face in the triangulated sphere. The column headers are ``x1``, ``y1``, ``z1``, ``x2``, ``y2``, ``z2``, ``x3``, ``y3``, ``z3``, reflecting the Cartesian coordinates of each vertex forming the face. """ return self._faces.copy()
def __init__( self, number_of_subdivisions: int = 3, number_of_shells: int = 1, magnitude_range: Optional[Tuple[float, float]] = None, ): # Create the sphere sphere = trimesh.primitives.Sphere( radius=1, subdivisions=number_of_subdivisions, mutable=False ) self._sphere = sphere # Get the data frame containing the faces face_index = pd.RangeIndex(start=0, stop=len(sphere.faces), name="face") vertex_coordinates = sphere.vertices[sphere.faces].reshape(-1, 9) faces_dataframe = pd.DataFrame( vertex_coordinates, index=face_index, columns=["x1", "y1", "z1", "x2", "y2", "z2", "x3", "y3", "z3"], ) self._faces = faces_dataframe super().__init__( number_of_shells=number_of_shells, magnitude_range=magnitude_range )
[docs] def _construct_orientation_index(self) -> pd.RangeIndex: """Get the orientation index for the current triangulated sphere. Produce the orientation index for the current triangulated sphere, containing all face indices for a given shell. Returns ------- pandas.RangeIndex Index containing all valid ``face`` indices. """ # Get the number of faces number_of_faces = len(self._faces) # Get the face indices face_indices = pd.RangeIndex(0, number_of_faces) return face_indices
[docs] def create_mesh(self) -> pv.PolyData: points = self._sphere.vertices faces = self._sphere.faces number_of_faces = len(faces) # Augment the faces by adding a column with 3s threes_column = np.ones(number_of_faces, dtype=int) * 3 threes_column = np.atleast_2d(threes_column).T complete_faces = np.concatenate([threes_column, faces], axis=-1) # And now, build the mesh sphere_mesh = pv.PolyData(points, complete_faces) # And now, just to be sure, let's put in the face scalars sphere_mesh.cell_data["face"] = range(number_of_faces) return sphere_mesh
[docs] def convert_vectors_to_cartesian_array( self, labelled_vectors: pd.DataFrame, create_unit_vectors: bool = False, include_spatial_coordinates: bool = False, ) -> np.ndarray: # So, the way that the frame is structured is that we have the # Cartesian components of the unit vectors as `ux, uy, uz` and then # we have the magnitude in the `magnitude` column. # First, let's extract the vector components. unit_vectors = labelled_vectors[["ux", "uy", "uz"]].to_numpy() # If we only want unit vectors, great! Return these! if create_unit_vectors: return unit_vectors magnitudes = labelled_vectors["magnitude"].to_numpy() magnitudes = np.expand_dims(magnitudes, axis=-1) cartesian_vectors = unit_vectors * magnitudes if include_spatial_coordinates: spatial_locations = labelled_vectors[["x", "y", "z"]].to_numpy() cartesian_vectors = np.concatenate( [spatial_locations, cartesian_vectors], axis=-1 ) return cartesian_vectors
[docs] def get_cell_indices(self, bins: pd.DataFrame) -> pd.Series: # Here, everything is already contained in the face column. return bins["face"]