Source code for pylops_mpi.proximal.ProxOperator
from mpi4py import MPI
from typing import Any
from pyproximal import ProxOperator
from pylops.utils.backend import get_module
from pylops_mpi import DistributedArray, Partition
_call_reduce_op = dict(
Box=(MPI.LAND, all),
L0=(MPI.SUM, sum),
L1=(MPI.SUM, sum),
)
[docs]
class MPIProxOperator:
"""MPI-enabled PyProximal Proximal Operator
Common interface for applying (separable) proximal operators in a
distributed fashion.
In practice, this class provides methods to compute the norm, proximal
operator and gradient between any :obj:`pyproximal.ProxOperator`
(which must be the same across ranks) and a :class:`pylops_mpi.DistributedArray`.
It internally handles the extraction of the local array from the distributed
array and the creation of the output :class:`pylops_mpi.DistributedArray`.
Parameters
----------
prox : :obj:`pyproximal.ProxOperator`
PyProximal Proximal Operator to wrap.
"""
def __init__(
self,
prox: ProxOperator,
) -> None:
# Check if prox is separable (by looking if is listed in
# the mapping dictionary)
prox_name = str(type(prox).__name__)
if prox_name not in _call_reduce_op:
raise NotImplementedError(
f"{prox_name} is not a separable proximal "
"operator, must be implemented directly...")
self.proxop = prox
self.hasgrad = prox.hasgrad
def __repr__(self) -> str:
if hasattr(self, "proxop"):
return f"<{type(self).__name__} ({type(self.proxop).__name__})>"
else:
return f"<{type(self).__name__}>"
def __call__(self, x: DistributedArray) -> DistributedArray:
"""Functional evaluation of the oprator.
Modified version of pyproximal `__call__`. This method makes use
of :class:`pylops_mpi.DistributedArray` to evaluate
the functional of the operator in a distributed fashion.
Parameters
----------
x : :obj:`pylops_mpi.DistributedArray`
A DistributedArray of global shape (N, ).
Returns
-------
f : :obj:`bool` or :obj:`float` or :obj:`int`
Function evaluation
"""
def _as_scalar(value):
"""Convert NumPy/CuPy/Python scalar-like objects to a Python scalar."""
# Ensure that a bool/int/float is returned
if isinstance(value, (bool, int, float)):
return value
ncp = get_module(x.engine)
if ncp.size(value) != 1:
raise ValueError(
f"Expected scalar function evaluation, "
f"got object with shape {getattr(value, 'shape', None)}"
)
return value.item()
if isinstance(x, DistributedArray):
# Compute local function evaluation
f = self.proxop(x.local_array)
if x.partition == Partition.SCATTER:
# Create receiver buffer
ncp = get_module(x.engine)
# Reduce local function evaluations into final evaluation
reduce_op = _call_reduce_op[str(type(self.proxop).__name__)][0]
recv_buf = x._allreduce_subcomm(x.sub_comm,
x.base_comm_nccl,
ncp.asarray(f),
op=reduce_op,
engine=x.engine)
return _as_scalar(recv_buf)
else:
# For broadcasted arrays, simply return the local evaluation
return _as_scalar(f)
else: # StackedDistributedArray
reduce_op = _call_reduce_op[str(type(self.proxop).__name__)][1]
fs = [self(x[iarr]) for iarr in range(x.narrays)]
f = reduce_op(fs)
return f
def prox(self, x: DistributedArray, tau: float, **kwargs: Any) -> DistributedArray:
"""Proximal operator applied to a vector
"""
if isinstance(x, DistributedArray):
y = x.empty_like()
y[:] = self.proxop.prox(x.local_array, tau)
else: # StackedDistributedArray
y = x.empty_like()
for iarr in range(x.narrays):
y[iarr][:] = self.proxop.prox(x[iarr].local_array, tau)
return y
def proxdual(self, x: DistributedArray, tau: float, **kwargs: Any) -> DistributedArray:
"""Dual Proximal operator applied to a vector
"""
y = DistributedArray(global_shape=x.global_shape,
base_comm=x.base_comm,
base_comm_nccl=x.base_comm_nccl,
partition=x.partition,
axis=x.axis,
local_shapes=x.local_shapes,
mask=x.mask,
engine=x.engine,
dtype=x.dtype)
y[:] = self.proxop.proxdual(x.local_array, tau)
return y