"""
Defines a family of algorithms for generating samples
The sample a for use with :class:`SALib.analyze.morris.analyze`,
encapsulate each one, and makes them interchangeable.
Example
-------
>>> localoptimisation = LocalOptimisation()
>>> context = SampleMorris(localoptimisation)
>>> context.sample(input_sample, num_samples, num_params, k_choices, groups)
"""
import abc
import numpy as np # type: ignore
from scipy.spatial.distance import cdist # type: ignore
[docs]class SampleMorris(object):
"""Computes the optimum `k_choices` of trajectories from the input_sample.
Parameters
----------
strategy : :class:`Strategy`
"""
def __init__(self, strategy):
self._strategy = strategy
[docs] def sample(self, input_sample, num_samples, num_params, k_choices, num_groups):
"""Computes the optimum k_choices of trajectories
from the input_sample.
Parameters
----------
input_sample : numpy.ndarray
num_samples : int
The number of samples to generate
num_params : int
The number of parameters
k_choices : int
The number of optimal trajectories
num_groups : int
The number of groups
Returns
-------
numpy.ndarray
An array of optimal trajectories
"""
return self._strategy.sample(
input_sample, num_samples, num_params, k_choices, num_groups
)
[docs]class Strategy:
"""
Declare an interface common to all supported algorithms.
:class:`SampleMorris` uses this interface to call the algorithm
defined by a ConcreteStrategy.
"""
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def _sample(self, input_sample, num_samples, num_params, k_choices, num_groups):
"""Implement this in your class
Parameters
----------
input_sample : numpy.ndarray
num_samples : int
The number of samples to generate
num_params : int
The number of parameters
k_choices : int
The number of optimal trajectories
num_groups : int
The number of groups
Returns
-------
list
A list of trajectory indices
"""
pass
[docs] def sample(self, input_sample, num_samples, num_params, k_choices, num_groups=None):
"""Computes the optimum k_choices of trajectories
from the input_sample.
Parameters
----------
input_sample : numpy.ndarray
num_samples : int
The number of samples to generate
num_params : int
The number of parameters
k_choices : int
The number of optimal trajectories
num_groups : int, default=None
The number of groups
Returns
-------
numpy.ndarray
"""
self.run_checks(num_samples, k_choices)
maximum_combo = self._sample(
input_sample, num_samples, num_params, k_choices, num_groups
)
assert isinstance(maximum_combo, list)
output = self.compile_output(
input_sample, num_samples, num_params, maximum_combo, num_groups
)
return output
[docs] @staticmethod
def run_checks(number_samples, k_choices):
"""Runs checks on `k_choices`"""
assert isinstance(
k_choices, int
), "Number of optimal trajectories should be an integer"
if k_choices < 2:
raise ValueError(
"The number of optimal trajectories must be set to 2 or more."
)
if k_choices >= number_samples:
msg = "The number of optimal trajectories should be less than the \
number of samples"
raise ValueError(msg)
@staticmethod
def _make_index_list(num_samples, num_params, num_groups=None):
"""Identify indices of input sample associated with each trajectory
For each trajectory, identifies the indexes of the input sample which
is a function of the number of factors/groups and the number of samples
Parameters
----------
num_samples : int
The number of trajectories
num_params : int
The number of parameters
num_groups : int
The number of groups
Returns
-------
list of numpy.ndarray
Example
-------
>>> BruteForce()._make_index_list(num_samples=4, num_params=3,
num_groups=2)
[np.array([0, 1, 2]), np.array([3, 4, 5]), np.array([6, 7, 8]),
np.array([9, 10, 11])]
"""
if num_groups is None:
num_groups = num_params
index_list = []
for j in range(num_samples):
index_list.append(np.arange(num_groups + 1) + j * (num_groups + 1))
return index_list
[docs] def compile_output(
self, input_sample, num_samples, num_params, maximum_combo, num_groups=None
):
"""Picks the trajectories from the input
Parameters
----------
input_sample : numpy.ndarray
num_samples : int
num_params : int
maximum_combo : list
num_groups : int
"""
if num_groups is None:
num_groups = num_params
self.check_input_sample(input_sample, num_groups, num_samples)
index_list = self._make_index_list(num_samples, num_params, num_groups)
output = np.zeros((np.size(maximum_combo) * (num_groups + 1), num_params))
for counter, combo in enumerate(maximum_combo):
output[index_list[counter]] = np.array(input_sample[index_list[combo]])
return output
[docs] @staticmethod
def compute_distance(m, l): # noqa: E741
"""Compute distance between two trajectories
Parameters
----------
m : np.ndarray
l : np.ndarray
Returns
-------
numpy.ndarray
"""
if np.shape(m) != np.shape(l):
raise ValueError("Input matrices are different sizes")
if np.array_equal(m, l):
# print("Trajectory %s and %s are equal" % (m, l))
distance = 0
else:
distance = np.array(np.sum(cdist(m, l)), dtype=np.float32)
return distance
[docs] def compute_distance_matrix(
self,
input_sample,
num_samples,
num_params,
num_groups=None,
local_optimization=False,
):
"""Computes the distance between each and every trajectory
Each entry in the matrix represents the sum of the geometric distances
between all the pairs of points of the two trajectories
If the `groups` argument is filled, then the distances are still
calculated for each trajectory,
Parameters
----------
input_sample : numpy.ndarray
The input sample of trajectories for which to compute
the distance matrix
num_samples : int
The number of trajectories
num_params : int
The number of factors
num_groups : int, default=None
The number of groups
local_optimization : bool, default=False
If True, fills the lower triangle of the distance matrix
Returns
-------
distance_matrix : numpy.ndarray
"""
if num_groups:
self.check_input_sample(input_sample, num_groups, num_samples)
else:
self.check_input_sample(input_sample, num_params, num_samples)
index_list = self._make_index_list(num_samples, num_params, num_groups)
distance_matrix = np.zeros((num_samples, num_samples), dtype=np.float32)
for j in range(num_samples):
input_1 = input_sample[index_list[j]]
for k in range(j + 1, num_samples):
input_2 = input_sample[index_list[k]]
# Fills the lower triangle of the matrix
if local_optimization is True:
distance_matrix[j, k] = self.compute_distance(input_1, input_2)
distance_matrix[k, j] = self.compute_distance(input_1, input_2)
return distance_matrix