from typing import Iterable, List, Literal, Tuple, Union
import autograd.numpy as np
from autograd.scipy.signal import convolve as convolve_ag
from numpy.typing import NDArray
from tidy3d.components.autograd.functions import interpn
from .types import PaddingType
__all__ = [
"interpn",
"pad",
"convolve",
"grey_dilation",
"grey_erosion",
"grey_opening",
"grey_closing",
"morphological_gradient",
"morphological_gradient_internal",
"morphological_gradient_external",
"rescale",
"threshold",
]
def _make_slices(rule: Union[int, slice], ndim: int, axis: int) -> Tuple[slice, ...]:
"""Create a tuple of slices for indexing an array.
Parameters
----------
rule : Union[int, slice]
The rule to apply on the specified axis.
ndim : int
The number of dimensions of the array.
axis : int
The axis to which the rule should be applied.
Returns
-------
Tuple[slice, ...]
A tuple of slices for indexing.
"""
return tuple(slice(None) if d != axis else rule for d in range(ndim))
def _constant_pad(
array: NDArray, pad_width: Tuple[int, int], axis: int, *, constant_value: float = 0.0
) -> NDArray:
"""Pad an array with a constant value along a specified axis.
Parameters
----------
array : NDArray
The input array to pad.
pad_width : Tuple[int, int]
The number of values padded to the edges of each axis.
axis : int
The axis along which to pad.
constant_value : float = 0.0
The constant value to pad with.
Returns
-------
NDArray
The padded array.
"""
p = [(pad_width[0], pad_width[1]) if ax == axis else (0, 0) for ax in range(array.ndim)]
return np.pad(array, p, mode="constant", constant_values=constant_value)
def _edge_pad(array: NDArray, pad_width: Tuple[int, int], axis: int) -> NDArray:
"""Pad an array using the `edge` mode along a specified axis.
Parameters
----------
array : NDArray
The input array to pad.
pad_width : Tuple[int, int]
The number of values padded to the edges of each axis.
axis : int
The axis along which to pad.
Returns
-------
NDArray
The padded array.
"""
left, right = (_make_slices(rule, array.ndim, axis) for rule in (0, -1))
cat_arys = []
if pad_width[0] > 0:
cat_arys.append(np.stack(pad_width[0] * [array[left]], axis=axis))
cat_arys.append(array)
if pad_width[1] > 0:
cat_arys.append(np.stack(pad_width[1] * [array[right]], axis=axis))
return np.concatenate(cat_arys, axis=axis)
def _reflect_pad(array: NDArray, pad_width: Tuple[int, int], axis: int) -> NDArray:
"""Pad an array using the `reflect` mode along a specified axis.
Parameters
----------
array : NDArray
The input array to pad.
pad_width : Tuple[int, int]
The number of values padded to the edges of each axis.
axis : int
The axis along which to pad.
Returns
-------
NDArray
The padded array.
"""
left, right = (
_make_slices(rule, array.ndim, axis)
for rule in (slice(pad_width[0], 0, -1), slice(-2, -pad_width[1] - 2, -1))
)
return np.concatenate([array[left], array, array[right]], axis=axis)
def _symmetric_pad(array: NDArray, pad_width: Tuple[int, int], axis: int) -> NDArray:
"""Pad an array using the `symmetric` mode along a specified axis.
Parameters
----------
array : NDArray
The input array to pad.
pad_width : Tuple[int, int]
The number of values padded to the edges of each axis.
axis : int
The axis along which to pad.
Returns
-------
NDArray
The padded array.
"""
left, right = (
_make_slices(rule, array.ndim, axis)
for rule in (
slice(pad_width[0] - 1, None, -1) if pad_width[0] > 0 else slice(0, 0),
slice(-1, -pad_width[1] - 1, -1),
)
)
return np.concatenate([array[left], array, array[right]], axis=axis)
def _wrap_pad(array: NDArray, pad_width: Tuple[int, int], axis: int) -> NDArray:
"""Pad an array using the `wrap` mode along a specified axis.
Parameters
----------
array : NDArray
The input array to pad.
pad_width : Tuple[int, int]
The number of values padded to the edges of each axis.
axis : int
The axis along which to pad.
Returns
-------
NDArray
The padded array.
"""
left, right = (
_make_slices(rule, array.ndim, axis)
for rule in (
slice(-pad_width[0], None) if pad_width[0] > 0 else slice(0, 0),
slice(0, pad_width[1]) if pad_width[1] > 0 else slice(0, 0),
)
)
return np.concatenate([array[left], array, array[right]], axis=axis)
[docs]
def pad(
array: NDArray,
pad_width: Union[int, Tuple[int, int]],
*,
mode: PaddingType = "constant",
axis: Union[int, Iterable[int], None] = None,
constant_value: float = 0.0,
) -> NDArray:
"""Pad an array along a specified axis with a given mode and padding width.
Parameters
----------
array : NDArray
The input array to pad.
pad_width : Union[int, Tuple[int, int]]
The number of values padded to the edges of each axis. If an integer is provided,
it is used for both the left and right sides. If a tuple is provided, it specifies
the padding for the left and right sides respectively.
mode : PaddingType = "constant"
The padding mode to use.
axis : Union[int, Iterable[int], None] = None
The axis or axes along which to pad. If None, padding is applied to all axes.
constant_value : float = 0.0
The value to set the padded values for "constant" mode.
Returns
-------
NDArray
The padded array.
Raises
------
ValueError
If the padding width has more than two elements or if padding is negative.
NotImplementedError
If padding larger than the input size is requested.
KeyError
If an unsupported padding mode is specified.
IndexError
If an axis is out of range for the array dimensions.
Examples
--------
>>> import numpy as np
>>> from tidy3d.plugins.autograd.functions import pad
>>> array = np.array([[1, 2], [3, 4]])
>>> pad(array, (1, 1), mode="constant", constant_value=0)
array([[0, 0, 0, 0],
[0, 1, 2, 0],
[0, 3, 4, 0],
[0, 0, 0, 0]])
>>> pad(array, 1, mode="reflect")
array([[4, 3, 4, 3],
[2, 1, 2, 1],
[4, 3, 4, 3],
[2, 1, 2, 1]])
"""
pad_width = np.atleast_1d(pad_width)
if pad_width.size == 1:
pad_width = np.array([pad_width[0], pad_width[0]])
elif pad_width.size != 2:
raise ValueError("Padding width must have one or two elements, got {pad_width.size}.")
if any(any(p >= s for p in pad_width) for s in array.shape):
raise NotImplementedError("Padding larger than the input size is not supported.")
if any(p < 0 for p in pad_width):
raise ValueError("Padding must be positive.")
if all(p == 0 for p in pad_width):
return array
if axis is None:
axis = range(array.ndim)
_mode_map = {
"constant": _constant_pad,
"edge": _edge_pad,
"reflect": _reflect_pad,
"symmetric": _symmetric_pad,
"wrap": _wrap_pad,
}
try:
pad_fun = _mode_map[mode]
except KeyError as e:
raise KeyError(f"Unsupported padding mode: {mode}.") from e
for ax in np.atleast_1d(axis):
if ax < 0:
ax += array.ndim
if ax < 0 or ax >= array.ndim:
raise IndexError(f"Axis out of range for array with {array.ndim} dimensions.")
array = pad_fun(array, pad_width, axis=ax)
return array
[docs]
def convolve(
array: NDArray,
kernel: NDArray,
*,
padding: PaddingType = "constant",
axes: Union[Tuple[List[int], List[int]], None] = None,
mode: Literal["full", "valid", "same"] = "same",
) -> NDArray:
"""Convolve an array with a given kernel.
Parameters
----------
array : NDArray
The input array to be convolved.
kernel : NDArray
The kernel to convolve with the input array. All dimensions of the kernel must be odd.
padding : PaddingType = "constant"
The padding mode to use.
axes : Union[Tuple[List[int], List[int]], None] = None
The axes along which to perform the convolution.
mode : Literal["full", "valid", "same"] = "same"
The convolution mode.
Returns
-------
NDArray
The result of the convolution.
Raises
------
ValueError
If any dimension of the kernel is even.
If the dimensions of the kernel do not match the dimensions of the array.
"""
if any(k % 2 == 0 for k in kernel.shape):
raise ValueError(f"All kernel dimensions must be odd, got {kernel.shape}.")
if kernel.ndim != array.ndim and axes is None:
raise ValueError(
f"Kernel dimensions must match array dimensions, got kernel {kernel.shape} and array {array.shape}."
)
if mode in ("same", "full"):
kernel_dims = kernel.shape if axes is None else [kernel.shape[d] for d in axes[1]]
pad_widths = [(ks // 2, ks // 2) for ks in kernel_dims]
for axis, pad_width in enumerate(pad_widths):
array = pad(array, pad_width, mode=padding, axis=axis)
mode = "valid" if mode == "same" else mode
return convolve_ag(array, kernel, axes=axes, mode=mode)
[docs]
def grey_dilation(
array: NDArray,
size: Union[Union[int, Tuple[int, int]], None] = None,
structure: Union[NDArray, None] = None,
*,
mode: PaddingType = "reflect",
maxval: float = 1e4,
) -> NDArray:
"""Perform grey dilation on an array.
Parameters
----------
array : NDArray
The input array to perform grey dilation on.
size : Union[Union[int, Tuple[int, int]], None] = None
The size of the structuring element. If None, `structure` must be provided.
structure : Union[NDArray, None] = None
The structuring element. If None, `size` must be provided.
mode : PaddingType = "reflect"
The padding mode to use.
maxval : float = 1e4
Value to assume for infinite elements in the kernel.
Returns
-------
NDArray
The result of the grey dilation operation.
Raises
------
ValueError
If both `size` and `structure` are None.
"""
if size is None and structure is None:
raise ValueError("Either size or structure must be provided.")
if size is not None:
size = np.atleast_1d(size)
shape = (size[0], size[-1])
nb = np.zeros(shape)
elif np.all(structure == 0):
nb = np.zeros_like(structure)
else:
nb = np.copy(structure)
nb[structure == 0] = -maxval
h, w = nb.shape
bias = np.reshape(nb, (-1, 1, 1))
kernel = np.reshape(np.eye(h * w), (h * w, h, w))
array = convolve(array, kernel, axes=((0, 1), (1, 2)), padding=mode) + bias
return np.max(array, axis=0)
[docs]
def grey_erosion(
array: NDArray,
size: Union[Union[int, Tuple[int, int]], None] = None,
structure: Union[NDArray, None] = None,
*,
mode: PaddingType = "reflect",
maxval: float = 1e4,
) -> NDArray:
"""Perform grey erosion on an array.
Parameters
----------
array : NDArray
The input array to perform grey dilation on.
size : Union[Union[int, Tuple[int, int]], None] = None
The size of the structuring element. If None, `structure` must be provided.
structure : Union[NDArray, None] = None
The structuring element. If None, `size` must be provided.
mode : PaddingType = "reflect"
The padding mode to use.
maxval : float = 1e4
Value to assume for infinite elements in the kernel.
Returns
-------
NDArray
The result of the grey dilation operation.
Raises
------
ValueError
If both `size` and `structure` are None.
"""
if size is None and structure is None:
raise ValueError("Either size or structure must be provided.")
if size is not None:
size = np.atleast_1d(size)
shape = (size[0], size[-1])
nb = np.zeros(shape)
elif np.all(structure == 0):
nb = np.zeros_like(structure)
else:
nb = np.copy(structure)
nb[structure == 0] = -maxval
h, w = nb.shape
bias = np.reshape(nb, (-1, 1, 1))
kernel = np.reshape(np.eye(h * w), (h * w, h, w))
array = convolve(array, kernel, axes=((0, 1), (1, 2)), padding=mode) - bias
return np.min(array, axis=0)
[docs]
def grey_opening(
array: NDArray,
size: Union[Union[int, Tuple[int, int]], None] = None,
structure: Union[NDArray, None] = None,
*,
mode: PaddingType = "reflect",
maxval: float = 1e4,
) -> NDArray:
"""Perform grey opening on an array.
Parameters
----------
array : NDArray
The input array to perform grey opening on.
size : Union[Union[int, Tuple[int, int]], None] = None
The size of the structuring element. If None, `structure` must be provided.
structure : Union[NDArray, None] = None
The structuring element. If None, `size` must be provided.
mode : PaddingType = "reflect"
The padding mode to use.
maxval : float = 1e4
Value to assume for infinite elements in the kernel.
Returns
-------
NDArray
The result of the grey opening operation.
"""
array = grey_erosion(array, size, structure, mode=mode, maxval=maxval)
array = grey_dilation(array, size, structure, mode=mode, maxval=maxval)
return array
[docs]
def grey_closing(
array: NDArray,
size: Union[Union[int, Tuple[int, int]], None] = None,
structure: Union[NDArray, None] = None,
*,
mode: PaddingType = "reflect",
maxval: float = 1e4,
) -> NDArray:
"""Perform grey closing on an array.
Parameters
----------
array : NDArray
The input array to perform grey closing on.
size : Union[Union[int, Tuple[int, int]], None] = None
The size of the structuring element. If None, `structure` must be provided.
structure : Union[NDArray, None] = None
The structuring element. If None, `size` must be provided.
mode : PaddingType = "reflect"
The padding mode to use.
maxval : float = 1e4
Value to assume for infinite elements in the kernel.
Returns
-------
NDArray
The result of the grey closing operation.
"""
array = grey_dilation(array, size, structure, mode=mode, maxval=maxval)
array = grey_erosion(array, size, structure, mode=mode, maxval=maxval)
return array
[docs]
def morphological_gradient(
array: NDArray,
size: Union[Union[int, Tuple[int, int]], None] = None,
structure: Union[NDArray, None] = None,
*,
mode: PaddingType = "reflect",
maxval: float = 1e4,
) -> NDArray:
"""Compute the morphological gradient of an array.
Parameters
----------
array : NDArray
The input array to compute the morphological gradient of.
size : Union[Union[int, Tuple[int, int]], None] = None
The size of the structuring element. If None, `structure` must be provided.
structure : Union[NDArray, None] = None
The structuring element. If None, `size` must be provided.
mode : PaddingType = "reflect"
The padding mode to use.
maxval : float = 1e4
Value to assume for infinite elements in the kernel.
Returns
-------
NDArray
The morphological gradient of the input array.
"""
return grey_dilation(array, size, structure, mode=mode, maxval=maxval) - grey_erosion(
array, size, structure, mode=mode, maxval=maxval
)
[docs]
def morphological_gradient_internal(
array: NDArray,
size: Union[Union[int, Tuple[int, int]], None] = None,
structure: Union[NDArray, None] = None,
*,
mode: PaddingType = "reflect",
maxval: float = 1e4,
) -> NDArray:
"""Compute the internal morphological gradient of an array.
Parameters
----------
array : NDArray
The input array to compute the internal morphological gradient of.
size : Union[Union[int, Tuple[int, int]], None] = None
The size of the structuring element. If None, `structure` must be provided.
structure : Union[NDArray, None] = None
The structuring element. If None, `size` must be provided.
mode : PaddingType = "reflect"
The padding mode to use.
maxval : float = 1e4
Value to assume for infinite elements in the kernel.
Returns
-------
NDArray
The internal morphological gradient of the input array.
"""
return array - grey_erosion(array, size, structure, mode=mode, maxval=maxval)
[docs]
def morphological_gradient_external(
array: NDArray,
size: Union[Union[int, Tuple[int, int]], None] = None,
structure: Union[NDArray, None] = None,
*,
mode: PaddingType = "reflect",
maxval: float = 1e4,
) -> NDArray:
"""Compute the external morphological gradient of an array.
Parameters
----------
array : NDArray
The input array to compute the external morphological gradient of.
size : Union[Union[int, Tuple[int, int]], None] = None
The size of the structuring element. If None, `structure` must be provided.
structure : Union[NDArray, None] = None
The structuring element. If None, `size` must be provided.
mode : PaddingType = "reflect"
The padding mode to use.
maxval : float = 1e4
Value to assume for infinite elements in the kernel.
Returns
-------
NDArray
The external morphological gradient of the input array.
"""
return grey_dilation(array, size, structure, mode=mode, maxval=maxval) - array
[docs]
def rescale(
array: NDArray, out_min: float, out_max: float, in_min: float = 0.0, in_max: float = 1.0
) -> NDArray:
"""
Rescale an array from an arbitrary input range to an arbitrary output range.
Parameters
----------
array : NDArray
The input array to be rescaled.
out_min : float
The minimum value of the output range.
out_max : float
The maximum value of the output range.
in_min : float = 0.0
The minimum value of the input range.
in_max : float = 1.0
The maximum value of the input range.
Returns
-------
NDArray
The rescaled array.
"""
if in_min == in_max:
raise ValueError(
f"'in_min' ({in_min}) must not be equal to 'in_max' ({in_max}) "
"to avoid division by zero."
)
if out_min >= out_max:
raise ValueError(f"'out_min' ({out_min}) must be less than 'out_max' ({out_max}).")
if in_min >= in_max:
raise ValueError(f"'in_min' ({in_min}) must be less than 'in_max' ({in_max}).")
scaled = (array - in_min) / (in_max - in_min)
return scaled * (out_max - out_min) + out_min
[docs]
def threshold(
array: NDArray, vmin: float = 0.0, vmax: float = 1.0, level: Union[float, None] = None
) -> NDArray:
"""Apply a threshold to an array, setting values below the threshold to `vmin` and values above to `vmax`.
Parameters
----------
array : NDArray
The input array to be thresholded.
vmin : float = 0.0
The value to assign to elements below the threshold.
vmax : float = 1.0
The value to assign to elements above the threshold.
level : Union[float, None] = None
The threshold level. If None, the threshold is set to the midpoint between `vmin` and `vmax`.
Returns
-------
NDArray
The thresholded array.
"""
if vmin >= vmax:
raise ValueError(
f"Invalid threshold range: 'vmin' ({vmin}) must be smaller than 'vmax' ({vmax})."
)
if level is None:
level = (vmin + vmax) / 2
elif not (vmin <= level <= vmax):
raise ValueError(
f"Invalid threshold level: 'level' ({level}) must be "
f"between 'vmin' ({vmin}) and 'vmax' ({vmax})."
)
return np.where(array < level, vmin, vmax)