import abc
import random
import typing
import numpy
from . import dataset
from . import metric
class Space(abc.ABC):
def __init__(self, use_cache: bool) -> None:
self.__use_cache = use_cache
self.__cache: dict[tuple[int, int], float] = {}
@property
def name(self) -> str:
return f"{self.data.name}__{self.distance_metric.name}"
@property
def uses_cache(self) -> bool:
return self.__use_cache
@property
@abc.abstractmethod
def data(self) -> dataset.Dataset:
pass
@property
@abc.abstractmethod
def distance_metric(self) -> metric.Metric:
pass
@abc.abstractmethod
def are_instances_equal(self, left: int, right: int) -> bool:
return self.distance_one_to_one(left, right) == 0.0
@abc.abstractmethod
def subset(self, indices: list[int], subset_data_name: str) -> "Space":
pass
@abc.abstractmethod
def distance_one_to_one(self, left: int, right: int) -> float:
if not self.is_in_cache(left, right):
d = self.distance_metric.one_to_one(self.data[left], self.data[right])
self.add_to_cache(left, right, d)
return self.get_from_cache(left, right)
@abc.abstractmethod
def distance_one_to_many(self, left: int, right: list[int]) -> numpy.ndarray:
distances = [self.distance_one_to_one(left, r) for r in right]
return numpy.asarray(distances, dtype=numpy.float32)
@abc.abstractmethod
def distance_many_to_many(self, left: list[int], right: list[int]) -> numpy.ndarray:
distances = [self.distance_one_to_many(l, right) for l in left]
return numpy.stack(distances)
@abc.abstractmethod
def distance_pairwise(self, indices: list[int]) -> numpy.ndarray:
return self.distance_many_to_many(indices, indices)
@staticmethod
def __cache_key(i: int, j: int) -> tuple[int, int]:
return (i, j) if i < j else (j, i)
def is_in_cache(self, i: int, j: int) -> bool:
return self.__cache_key(i, j) in self.__cache
def get_from_cache(self, i: int, j: int) -> float:
return self.__cache[self.__cache_key(i, j)]
def add_to_cache(self, i: int, j: int, distance: float):
self.__cache[self.__cache_key(i, j)] = distance
def remove_from_cache(self, i: int, j: int) -> float:
return self.__cache.pop(self.__cache_key(i, j), default=0.0)
def clear_cache(self) -> int:
num_items = len(self.__cache)
self.__cache.clear()
return num_items
def choose_unique(self, n: int, indices: list[int] = None) -> list[int]:
indices = indices or list(range(self.data.cardinality))
if not (0 < n <= len(indices)):
msg = f"`n` must be a positive integer no larger than the length of `indices` ({len(indices)}). Got {n} instead."
raise ValueError(
msg,
)
randomized_indices = indices.copy()
random.shuffle(randomized_indices)
if n == len(randomized_indices):
return randomized_indices
chosen: list[int] = []
for i in randomized_indices:
for o in chosen:
if self.are_instances_equal(i, o):
break
else:
chosen.append(i)
if len(chosen) == n:
break
return chosen
class TabularSpace(Space):
def __init__(
self,
data: typing.Union[dataset.TabularDataset, dataset.TabularMMap],
distance_metric: metric.Metric,
use_cache: bool,
) -> None:
self.__data = data
self.__distance_metric = distance_metric
super().__init__(use_cache)
@property
def data(self) -> typing.Union[dataset.TabularDataset, dataset.TabularMMap]:
return self.__data
@property
def distance_metric(self) -> metric.Metric:
return self.__distance_metric
def are_instances_equal(self, left: int, right: int) -> bool:
return self.distance_one_to_one(left, right) == 0.0
def distance_one_to_one(self, left: int, right: int) -> float:
if self.uses_cache:
return super().distance_one_to_one(left, right)
else:
return self.distance_metric.one_to_one(self.data[left], self.data[right])
def distance_one_to_many(self, left: int, right: list[int]) -> numpy.ndarray:
if self.uses_cache:
return super().distance_one_to_many(left, right)
else:
return self.distance_metric.one_to_many(self.data[left], self.data[right])
def distance_many_to_many(self, left: list[int], right: list[int]) -> numpy.ndarray:
if self.uses_cache:
return super().distance_many_to_many(left, right)
else:
return self.distance_metric.many_to_many(self.data[left], self.data[right])
def distance_pairwise(self, indices: list[int]) -> numpy.ndarray:
return self.distance_metric.pairwise(self.data[indices])
def subset(self, indices: list[int], subset_data_name: str) -> "TabularSpace":
return TabularSpace(
self.data.subset(indices, subset_data_name),
self.distance_metric,
self.uses_cache,
)
__all__ = [
"Space",
"TabularSpace",
]