import numpy as np
from typing import Union, Tuple, Callable
from enum import IntEnum
[docs]class CombineTypes(IntEnum):
"""Specifies how the constraints should act on the raster data.
For land-coverage the combine type can be :attr:`fill` or :attr:`coverage`.
"""
fill = 1
"""A cell value indicates the fraction within this cell.
With this combine type an inlay will first compute the total amount of
land coverage within a bounding box and then try to redistribute the
amount within the cells.
"""
coverage = 2
"""Indicates the fraction of cells within the bounding box that should
be covered. Using this combine type only bounding boxes in which all
cells have the same value are processed.
"""
[docs]class InvalidCellsIn(IntEnum):
"""Used to indicate where invalid (or outside) cells should be detected.
"""
none = 0
"""Indicates that there are no invalid (or outside) cells.
"""
raster = 1
"""Check for invalid cells in the raster block.
"""
constraints = 2
"""Check for invalid cells in the constraints block.
"""
[docs]class MultivaluedBlockError(Exception):
pass
[docs]class InvalidBlockError(Exception):
pass
[docs]class BlockProcessingError(Exception):
pass
[docs]def exclude_invalid(
inlay_form: Union[Tuple[slice, slice], np.ndarray],
raster: np.ndarray,
constraints: np.ndarray,
invalid_in: InvalidCellsIn,
invalid_value: Union[int, float],
inplace: bool = True
):
"""Get raster and constraints data in `inlay_form` excluding invalid cells
.. warning::
Incomplete and unused function.
"""
raster_data = None
return raster_data
[docs]def fraction_inlay(
raster_block: np.ndarray,
const_block: np.ndarray,
# inlay_form: Union[Tuple[slice, slice], np.ndarray],
cell_unit: Union[int, float] = 1,
cell_max: int = 100,
invalid_in: InvalidCellsIn = InvalidCellsIn.none,
invalid_value: int = 254,
) -> Tuple[np.ndarray, Union[int, None]]:
"""Redistribute the values in :attr:`raster_block` to avoid collision with
the values in :attr:`const_block`
Parameters
==========
Returns
=======
tuple:
np.ndarray:
The inlayed raster block.
Union[int, None]:
The capacity remaining in the raster block.
.. note::
A value below zero indicates a jammed block.
The actual amount gives the excess data from the
:attr:`raster_block` that could no longer be fitted in.
"""
# print(f'{raster_data=}')
data_shape = raster_block.shape
# print(f'{data_shape=}')
# TODO: move these tests out into the bulk bbox processing function
if invalid_in == InvalidCellsIn.none:
valid_raster_block = raster_block
valid_const_block = const_block
else:
if invalid_in == InvalidCellsIn.constraints:
invalid_mask = const_block != invalid_value
_with_invalid = const_block
elif invalid_in == InvalidCellsIn.raster:
invalid_mask = raster_block != invalid_value
_with_invalid = raster_block
valid_raster_block = raster_block[invalid_mask]
valid_const_block = const_block[invalid_mask]
# the number of cells in raster in the inlay form
raster_nbr = valid_raster_block.size
if not raster_nbr:
# There are no valid cells so we stop here.
return raster_block, None
const_nbr = valid_raster_block.size
# get totals
raster_total = np.sum(valid_raster_block)
const_total = np.sum(valid_const_block)
assert raster_nbr == const_nbr, 'not same size for both masked blocks'
# print(f'{const_view=}')
# set the form max in within cell units
# c_data_form_max = raster_nbr * cell_max
block_max = raster_nbr * cell_max
# ###
# if unique value in raster_block
# ###
# get the amount to inlay within the form on a per-cell level
# get unique value of raster data (in the form)
# block_uniques = np.unique(valid_raster_block)
# try:
# int(block_uniques[0])
# except TypeError:
# raise MultivaluedBlockError(
# 'Valid cells in raster block contain multiple values')
# except IndexError:
# # block contains no valid cells
# # TODO: How should this be treated?
# return None
# ###
# ###
# FIXME: remove later - only for testing
_data_total_initial = raster_total
# not sure if this is used
# data_total = np.sum(raster_data)
if raster_total > block_max:
# TODO: this could be an error.
print("WARNING: Valid cells in block cannot carry the required amount"
", even without constraints")
# check if non of the cells is already over-filled by constraints
assert np.all(valid_const_block <= cell_max), 'constraints block is '\
'initially overloaded!'
# NOTE: we do not mind if valid raster block cells are initially overloaded
# check if data and constrains fit into the form together
# already set the remaining capacity for the form
# negative as over-filled
valid_block_total = const_total + raster_total
# give the capacity
block_capacity = block_max - valid_block_total
if valid_block_total > block_max:
print("WARNING: Valid cells in the block cannot accommodate the "
"data along with the constraints!")
# we can directly set the raster data
if invalid_in == InvalidCellsIn.none:
raster_block = cell_max - const_block
else:
raster_block = np.where(
_with_invalid != invalid_value,
cell_max - const_block,
raster_block
)
else:
# data_form can be redistributed
# wipe the form clean
if invalid_in == InvalidCellsIn.none:
raster_block[:] = 0
else:
# set the block to 0 in all valid cells
_raster_block_orig = raster_block.copy()
raster_block[:] = np.where(
_with_invalid != invalid_value,
0, invalid_value
)
# fill incrementally, always the smallest cell
while raster_total:
# print(f'{data_total=}')
# print(f'{raster_data=}')
# print(f'{const_view=}')
# NOTE: this only works as long as invalid value is bigger than
# cell_max!
min_ind = np.unravel_index(
np.argmin(raster_block + const_block, axis=None),
data_shape
)
raster_block[min_ind] += cell_unit
assert raster_block[min_ind] <= cell_max
raster_total -= cell_unit
if invalid_in != InvalidCellsIn.none:
raster_block[:] = np.where(
_with_invalid != invalid_value,
raster_block, _raster_block_orig
)
# check if non of the raster data got lost
if block_capacity >= 0:
assert _data_total_initial == np.sum(raster_block[invalid_mask])
return raster_block, block_capacity
[docs]def inlay(
raster: np.ndarray,
constraints: np.ndarray,
inlay_bbox: Tuple[slice, slice],
set_inlay: Callable[
[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray]
],
set_inlay_kwargs: dict
) -> Union[float, bool]:
"""
.. todo::
Exclude values in the raster for invalid cells defined in constraints.
Parameters
==========
invalid_value:
Cells with this value are considered as masked and completely excluded
Returns
=======
float:
The remaining capacity within the form. A negative value indicates
that the inlay_form could not accommodate the data from raster
completely.
"""
raster_view = raster[inlay_bbox]
const_view = constraints[inlay_bbox]
# pass the views to the set_inlay function
try:
raster_view[:], block_capacity = set_inlay(
raster_view, const_view, **set_inlay_kwargs
)
except MultivaluedBlockError:
raise BlockProcessingError
except InvalidBlockError:
# Note: unused for now
return None
return block_capacity
[docs]def imprint_constraints(
raster,
constraints,
bblocks,
inlay_callback_kwargs: dict,
data_type: CombineTypes = CombineTypes.fill
) -> Tuple[dict, dict]:
"""
.. warning::
Function in under construction.
"""
# TODO: add option to specify data type
if data_type == CombineTypes.fill:
inlay_cb = fraction_inlay
else:
raise NotImplementedError()
capacities = {}
block_report = dict(ok=[], jammed=[], ignored=[], failed=[])
for i, bblock in enumerate(bblocks):
try:
_capacity = inlay(
raster, constraints, bblock,
inlay_cb, inlay_callback_kwargs)
except BlockProcessingError:
block_report['failed'].append(i)
print(f'{bblock=}')
else:
if _capacity is not None:
capacities[i] = _capacity
if _capacity < 0:
block_report['jammed'].append(i)
else:
block_report['ok'].append(i)
else:
block_report['ignored'].append(i)
return block_report, capacities