from __future__ import annotations
from typing import Any
import numpy as np
from VeraGridEngine.basic_structures import IntVec
from VeraGridEngine.Devices.Profiles.type_checks import coerce_bool_value
[docs]
class SparseArrayBool:
"""
Sparse array specialized for ``bool`` values.
"""
__slots__ = (
"_dtype",
"_default_value",
"_size",
"_map",
)
def __init__(self, default_value: bool, size: int = 0) -> None:
"""
Build a boolean sparse array.
:param default_value: Default sparse value.
:param size: Logical array size.
"""
self._dtype: type[bool] = bool
self._default_value: bool = coerce_bool_value(default_value)
self._size: int = size
self._map: dict[int, bool] = dict()
[docs]
def copy(self) -> "SparseArrayBool":
"""
Build a deep copy of the sparse array.
:return: New sparse array copy.
"""
copied_array: SparseArrayBool = SparseArrayBool(default_value=self._default_value, size=self._size)
copied_array._map = self._map.copy()
return copied_array
@property
def dtype(self) -> type[bool]:
"""
Get the declared type.
:return: ``bool``.
"""
return self._dtype
@property
def default_value(self) -> bool:
"""
Get the default sparse value.
:return: Default sparse value.
"""
return self._default_value
@default_value.setter
def default_value(self, val: bool) -> None:
"""
Change the default sparse value while preserving the represented dense data.
:param val: New default value.
:return: ``None``.
"""
coerced_value: bool = coerce_bool_value(val)
if coerced_value != self._default_value:
if self._size > 0 and len(self._map) < self._size:
old_default_value: bool = self._default_value
# Materialize implicit positions before changing the default so values do not change logically.
for idx in range(self._size):
if idx not in self._map:
self._map[idx] = old_default_value
else:
self._map[idx] = self._map[idx]
else:
pass
keys_to_remove: list[int] = list()
for key, value in self._map.items():
if value == coerced_value:
keys_to_remove.append(key)
else:
keys_to_remove = keys_to_remove
for key in keys_to_remove:
del self._map[key]
self._default_value = coerced_value
else:
self._default_value = self._default_value
[docs]
def info(self) -> dict[str, Any]:
"""
Get diagnostic information.
:return: Information dictionary.
"""
return {
"me": hex(id(self)),
"default_value": self._default_value,
"size": self._size,
"map": hex(id(self._map)),
}
[docs]
def get_map(self) -> dict[int, bool]:
"""
Get the sparse map.
:return: Sparse index-value map.
"""
return self._map
[docs]
def insert(self, i: int, x: bool) -> None:
"""
Insert a sparse value explicitly.
:param i: Target index.
:param x: Value to store.
:return: ``None``.
"""
self._map[i] = coerce_bool_value(x)
[docs]
def get_sparsity(self) -> float:
"""
Get the stored-entry ratio.
:return: Sparsity ratio.
"""
if self._size > 0:
return float(len(self._map)) / float(self._size)
else:
return 0.0
[docs]
def create(self, size: int, default_value: bool, data: dict[int, bool] | None = None) -> "SparseArrayBool":
"""
Create the sparse array from explicit sparse data.
:param size: Logical size.
:param default_value: Default sparse value.
:param data: Sparse map.
:return: ``self``.
"""
self.default_value = default_value
self._size = size
if data is None:
self._map = dict()
else:
self.set_data(data)
return self
[docs]
def create_from_array(self, array: np.ndarray, default_value: bool) -> "SparseArrayBool":
"""
Create the sparse array from a dense array.
:param array: Dense boolean array.
:param default_value: Default sparse value.
:return: ``self``.
"""
dense_array: np.ndarray = np.asarray(array, dtype=bool)
self.default_value = default_value
self._size = len(dense_array)
self._map = dict()
# The dense boolean path uses vectorized comparison to identify the sparse coordinates quickly.
non_default_indices: np.ndarray = np.flatnonzero(dense_array != self._default_value)
for raw_idx in non_default_indices:
idx: int = int(raw_idx)
self._map[idx] = bool(dense_array[idx])
return self
[docs]
def create_from_dict(self, default_value: bool, size: int, map_data: dict[int, bool]) -> "SparseArrayBool":
"""
Create the sparse array from a sparse map.
:param default_value: Default sparse value.
:param size: Logical size.
:param map_data: Sparse map.
:return: ``self``.
"""
self.default_value = default_value
self._size = size
self.set_data(map_data)
return self
[docs]
def fill(self, value: bool) -> None:
"""
Fill the logical array with the same value.
:param value: Fill value.
:return: ``None``.
"""
self.default_value = value
self._map = dict()
[docs]
def toarray(self) -> np.ndarray:
"""
Expand the sparse array into a dense numpy array.
:return: Dense boolean array.
"""
dense_array: np.ndarray = np.full(self._size, self._default_value, dtype=bool)
# Scatter the explicitly stored sparse entries into the dense output.
for key, value in self._map.items():
dense_array[key] = value
return dense_array
[docs]
def at(self, idx: int) -> bool:
"""
Get a value at a given position.
:param idx: Position to read.
:return: Value at ``idx``.
"""
if len(self._map) == 0:
return self._default_value
else:
value: bool | None = self._map.get(idx, None)
if value is None:
return self._default_value
else:
return value
def __getitem__(self, key: int) -> bool:
"""
Get a value using index syntax.
:param key: Position to read.
:return: Value at ``key``.
"""
return self.at(idx=key)
def __setitem__(self, key: int, value: bool) -> None:
"""
Set a value using index syntax.
:param key: Position to write.
:param value: Value to store.
:return: ``None``.
"""
if isinstance(key, int):
if key < self._size:
coerced_value: bool = coerce_bool_value(value)
if coerced_value != self._default_value:
self._map[key] = coerced_value
else:
if key in self._map:
del self._map[key]
else:
self._map = self._map
else:
raise AssertionError("Key out of bounds")
else:
raise TypeError("Key must be an integer")
def __eq__(self, other: object) -> bool:
"""
Compare two sparse arrays.
:param other: Object to compare against.
:return: ``True`` when both sparse arrays match.
"""
if isinstance(other, SparseArrayBool):
if self._default_value != other._default_value:
return False
else:
if self._size != other._size:
return False
else:
if self._map != other._map:
return False
else:
return True
else:
return False
[docs]
def size(self) -> int:
"""
Get the logical size.
:return: Logical size.
"""
return self._size
[docs]
def clear(self) -> None:
"""
Clear the sparse contents.
:return: ``None``.
"""
self._map.clear()
self._size = 0
[docs]
def set_data(self, d: dict[int, bool]) -> None:
"""
Replace the sparse map.
:param d: New sparse map.
:return: ``None``.
"""
new_map: dict[int, bool] = dict()
for key, value in d.items():
coerced_key: int = int(key)
coerced_value: bool = coerce_bool_value(value)
if coerced_value != self._default_value:
new_map[coerced_key] = coerced_value
else:
new_map = new_map
self._map = new_map
[docs]
def resize(self, n: int) -> None:
"""
Resize the sparse array.
:param n: New logical size.
:return: ``None``.
"""
if n < self._size:
keys_to_remove: list[int] = list()
for key in self._map.keys():
if key >= n:
keys_to_remove.append(key)
else:
keys_to_remove = keys_to_remove
for key in keys_to_remove:
del self._map[key]
else:
pass
self._size = n
[docs]
def resample(self, indices: IntVec) -> None:
"""
Resample the sparse array in place.
:param indices: New index selection.
:return: ``None``.
"""
self._size = len(indices)
new_map: dict[int, bool] = dict()
# Reindex only the explicitly stored values that survive the new selection.
for new_idx, old_idx in enumerate(indices):
sparse_value: bool | None = self._map.get(int(old_idx), None)
if sparse_value is not None:
new_map[new_idx] = sparse_value
else:
new_map = new_map
self._map = new_map
[docs]
def slice(self, indices: IntVec) -> "SparseArrayBool":
"""
Get a resampled copy.
:param indices: New index selection.
:return: Resampled sparse array.
"""
sliced_array: SparseArrayBool = self.copy()
sliced_array.resample(indices)
return sliced_array
[docs]
def get_sparse_representation(self) -> tuple[list[int], list[bool]]:
"""
Export the sparse contents as parallel index and value lists.
:return: Tuple ``(indices, values)``.
"""
indptr: list[int] = list()
data: list[bool] = list()
for idx, value in self._map.items():
indptr.append(idx)
data.append(value)
return indptr, data
[docs]
def set_sparse_data_from_data(self, indptr: list[int], data: list[bool]) -> None:
"""
Load sparse data from parallel lists.
:param indptr: Sparse indices.
:param data: Sparse values.
:return: ``None``.
"""
self._map = dict()
for idx, value in zip(indptr, data):
self._map[int(idx)] = coerce_bool_value(value)