Source code for vectorose.mock_data

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


"""
Mock vector data creator.

This module provides tools to create artificial vectors for testing.

Warnings
--------
This module does not exist as an automatic top-level import in vectorose,
and must therefore be explicitly imported.

References
----------
.. [#fisher-lewis-embleton] Fisher, N. I., Lewis, T., & Embleton, B. J.
   J. (1993). Statistical analysis of spherical data ([New ed.], 1.
   paperback ed). Cambridge Univ. Press.
"""
from collections.abc import Collection
from typing import List, Optional, Union

import numpy as np
from scipy.stats import vonmises_fisher

from . import util
from .util import convert_spherical_to_cartesian_coordinates


[docs] def create_vonmises_fisher_vectors_single_direction( phi: float, theta: float, kappa: float, number_of_points: int, magnitude: float = 1.0, magnitude_std: float = 0, use_degrees: bool = False, seed: Optional[int] = None ) -> np.ndarray: """Create a set of vectors using a von Mises-Fisher distribution. Draw a set of random orientations from a von Mises-Fisher distribution on the unit sphere. The magnitude of these vectors can be modified using a normal distribution. These vectors are represented by components without any spatial coordinates. Parameters ---------- phi Mean phi value, where phi reflects the inclination from the positive z-axis. theta Mean theta value, where theta reflects the in-plane angle clockwise from the positive y-axis. kappa Concentration parameter for the von Mises-Fisher distribution. number_of_points Number of points to draw from the distribution. magnitude Average magnitude for the computed vectors. magnitude_std Standard deviation for the magnitude distribution. If this is a positive value, the magnitudes follow a Gaussian distribution with mean `magnitude`. use_degrees Indicate whether the angles `phi` and `theta` are provided in degrees. seed Optional seed for random number generation. Returns ------- numpy.ndarray Array of shape ``(number_of_points, 3)`` containing the generated vectors. See Also -------- scipy.stats.vonmises_fisher : Function used to generate the von Mises-Fisher distribution. Notes ----- This function relies on the von Mises-Fisher distribution, which is a true probability distribution on the sphere. """ # Convert the mean direction to cartesian coordinates mu_spherical = np.array([phi, theta]) if use_degrees: mu_spherical = np.radians(mu_spherical) mu = convert_spherical_to_cartesian_coordinates(angular_coordinates=mu_spherical) # Generate the von Mises-Fisher distribution vmf = vonmises_fisher(mu=mu, kappa=kappa, seed=seed) # Sample the distribution sampled_points = vmf.rvs(size=number_of_points) # Play with the magnitude, if applicable. if magnitude_std > 0: # Sample the magnitudes from a Gaussian distribution magnitudes = np.random.default_rng(seed).normal( loc=magnitude, scale=magnitude_std, size=number_of_points ) # Multiply the components by the respective magnitudes sampled_points *= magnitudes[:, None] else: # Rescale the vectors by the specified magnitude sampled_points *= magnitude # Return the sampled points return sampled_points
[docs] def convert_args_to_length( n: int, *args: Union[float, Collection[float]] ) -> tuple[np.ndarray, ...]: """Standardise the length of all arguments. Convert the provided numbers or collections of numbers to NumPy arrays of a specified length. Parameters ---------- n The length to which all fields will be standardised. *args The arguments to convert to arrays of a specified length. Each must either be a single value (or a collection of length 1) or a collection of length `n`. Returns ------- tuple of numpy.ndarray The converted values in the same order they were originally passed. Any NumPy arrays passed will **not be copied** and the original arrays will simply be returned. Raises ------ ValueError If collections passed in have a length that is not 1 or `n`. """ converted_arguments: List[np.ndarray] = [] for arg in args: # Check to see if we have a collection if isinstance(arg, Collection): if isinstance(arg, np.ndarray): converted_arg = arg else: converted_arg = np.array(arg) if len(converted_arg) == 1: converted_arg = np.tile(converted_arg, n) if converted_arg.ndim > 1 or len(converted_arg) != n: raise ValueError("The passed arguments must have length 1 or `n`!") else: converted_arg = np.tile(arg, n) converted_arguments.append(converted_arg) return tuple(converted_arguments)
[docs] def create_von_mises_fisher_vectors_multiple_directions( phis: Collection[float], thetas: Collection[float], kappas: Collection[float], numbers_of_vectors: Union[int, Collection[int]] = 1000, magnitudes: Union[float, Collection[float]] = 1.0, magnitude_stds: Union[float, Collection[float]] = 0.5, use_degrees: bool = False, seeds: Optional[Collection[int]] = None ) -> np.ndarray: """Create vectors drawn from multiple von Mises-Fisher distributions. Using the supplied arguments, generate a collection of vectors drawn from multiple von Mises-Fisher distributions. These vectors may have non-unit magnitude, determined using a Gaussian distribution. Parameters ---------- phis The set of ``phi`` values for the mean direction. thetas The set of ``theta`` values for the mean direction. kappas The set of concentration parameters for the distributions. If a single :class:`float` is passed, the same concentration parameter will be used for each set of vectors. numbers_of_vectors Number of vectors to produce for each parameter set. If a single :class:`int` is passed, the same number of vectors will be generated for each parameter set. magnitudes The average magnitude of the vectors produced for each parameter set. If a single :class:`float` is passed, then the same average magnitude is used for all parameter sets. magnitude_stds The standard deviation of the magnitude for each parameter set. If greater than zero, then the magnitudes are drawn from a normal distribution. If a single :class:`float` is passed, then the same standard deviation is used for all parameter sets. use_degrees Indicate whether the provided angles are in degrees. If `False`, the angles are assumed to be in radians. seeds Optional seeds for the random number generation for reproducibility. Returns ------- numpy.ndarray The generated vectors drawn from different von Mises-Fisher distributions. Warnings -------- The array-like arguments must **all** have the same length, unless a single value is provided. See Also -------- create_vonmises_fisher_vectors_single_direction : Function that generates vectors drawn from a single von Mises-Fisher distribution. """ # Convert everything to arrays phi_array: np.ndarray = np.array(phis) theta_array: np.ndarray = np.array(thetas) kappa_array: np.ndarray = np.array(kappas) seed_array: Optional[np.ndarray] if seeds is not None: seed_array = np.array(seeds) else: seed_array = None # Get the number of vector families number_of_families = len(phi_array) # Convert the remaining arguments ( number_of_vectors_array, magnitude_array, magnitude_std_array, seeds_array ) = convert_args_to_length( number_of_families, numbers_of_vectors, magnitudes, magnitude_stds, seed_array ) # Now, build up the results vector_results = [ create_vonmises_fisher_vectors_single_direction( phi_array[i], theta_array[i], kappa_array[i], number_of_vectors_array[i], magnitude_array[i], magnitude_std_array[i], use_degrees, seeds_array[i] ) for i in range(number_of_families) ] all_vectors = np.concatenate(vector_results, axis=0) return all_vectors
[docs] def generate_watson_distribution( mean_direction: np.ndarray, kappa: float, n: int = 100000, seed: Optional[int] = None ) -> np.ndarray: """Generate points from a Watson distribution. Simulate a orientations from a Watson distribution using the steps presented by Fisher, Lewis and Embleton [#fisher-lewis-embleton]_ in section 3.6.2. Parameters ---------- mean_direction Cartesian coordinates of the mean direction. kappa Shape parameter of the watson distribution. n Number of points to generate. seed Optional seed for the random number generator. Returns ------- numpy.ndarray Array with `n` rows, corresponding to the 3D Cartesian coordinates of the pseudo-randomly generated points. """ # random_vectors = np.zeros((n, 3)) random_vector_list: List[np.ndarray] = [] rng = np.random.default_rng(seed) for _ in range(n): # Check the shape parameter s = 0 if kappa > 0: while True: # Construct the bipolar distribution c = 1 / (np.exp(kappa) - 1) u = rng.uniform() v = rng.uniform() s = (1 / kappa) * np.log(u / c + 1) if v <= np.exp(kappa * s * s - kappa * s): break else: while True: # Construct the girdle distribution c1 = np.sqrt(np.abs(kappa)) c2 = np.arctan(c1) u = rng.uniform() v = rng.uniform() s = (1 / c1) * np.tan(c2 * u) if v <= (1 - kappa * s * s) * np.exp(kappa * s * s): break # Perform the common steps # Compute the co-latitude and the longitude - adapt for our # definition of phi and theta phi = np.arccos(s) theta = 2 * np.pi * rng.uniform() # Add the new vector spherical angles to the list new_vector = np.array([phi, theta]) random_vector_list.append(new_vector) # Convert all new vectors to cartesian coordinates and rotate to mean random_vectors = np.stack(random_vector_list, axis=0) new_vectors_cartesian = util.convert_spherical_to_cartesian_coordinates( random_vectors ) rotated_new_vectors = util.rotate_vectors( new_vectors_cartesian, new_pole=mean_direction ) return rotated_new_vectors
[docs] def generate_watson_vectors_multiple_directions( phis: Collection[float], thetas: Collection[float], kappas: Collection[float], numbers_of_vectors: Union[int, Collection[int]] = 1000, use_degrees: bool = False, seeds: Optional[Collection[int]] = None ) -> np.ndarray: """Create vectors drawn from multiple von Watson distributions. Using the supplied arguments, generate a collection of vectors drawn from multiple Watson distributions. Parameters ---------- phis The set of ``phi`` values for the mean direction. thetas The set of ``theta`` values for the mean direction. kappas The set of shape parameters for the distributions. If a single :class:`float` is passed, the same shape parameter will be used for each set of vectors. numbers_of_vectors Number of vectors to produce for each parameter set. If a single :class:`int` is passed, the same number of vectors will be generated for each parameter set. use_degrees Indicate whether the provided angles are in degrees. If `False`, the angles are assumed to be in radians. seeds Optional seeds for the random number generation for reproducibility. Returns ------- numpy.ndarray The generated vectors drawn from different Watson distributions. Warnings -------- The array-like arguments must **all** have the same length, unless a single value is provided. See Also -------- generate_watson_distribution : Function that generates vectors drawn from a Watson distribution. create_von_mises_fisher_vectors_multiple_directions : Similar function for generating vectors from multiple von Mises-Fisher distributions. """ # Convert everything to arrays phi_array: np.ndarray = np.array(phis) theta_array: np.ndarray = np.array(thetas) kappa_array: np.ndarray = np.array(kappas) seed_array: Optional[np.ndarray] if seeds is not None: seed_array = np.array(seeds) else: seed_array = None # Get the number of vector families number_of_families = len(phi_array) # Convert the remaining arguments (number_of_vectors_array, seeds_array ) = convert_args_to_length( number_of_families, numbers_of_vectors, seed_array ) # Convert the phi and theta values to Cartesian coordinates stacked_angles = np.stack([phi_array, theta_array], axis=-1) # Convert the angles to radians, if necessary. if use_degrees: stacked_angles = np.radians(stacked_angles) mean_directions = util.convert_spherical_to_cartesian_coordinates(stacked_angles) # Now, build up the results vector_results = [ generate_watson_distribution( mean_directions[i], kappa_array[i], number_of_vectors_array[i], seeds_array[i] ) for i in range(number_of_families) ] all_vectors = np.concatenate(vector_results, axis=0) return all_vectors