from copy import copy
from functools import partial
from typing import Callable
from golem.utilities.singleton_meta import SingletonMeta
[docs]class AdaptRegistry(metaclass=SingletonMeta):
"""Registry of callables that require adaptation of argument/return values.
AdaptRegistry together with :py:class:`golem.core.adapter.adapter.BaseOptimizationAdapter`
enables automatic transformation between internal and domain graph representations.
**Short description of the use-case**
Operators & verification rules that operate on internal representation
of graphs must be marked as native with decorator
:py:func:`golem.core.adapter.adapt_registry.register_native`.
Usually this is the case when users of the framework provide custom
operators for internal optimization graphs. When custom operators
operate on domain graphs, nothing is required.
**Extended description**
Optimiser operates with generic graph representation.
Because of this any domain function requires adaptation
of its graph arguments. Adapter can automatically adapt
arguments to generic form in such cases.
Important notions:
* 'Domain' functions operate with domain-specific graphs.
* 'Native' functions operate with generic graphs used by optimiser.
* 'External' functions are functions defined by users of optimiser.
Most notably, custom mutations and custom verifier rules.
* 'Internal' functions are those defined by graph optimiser.
Most notably, the default set of mutations and verifier rules.
All internal functions are native.
Adaptation registry usage and behavior:
* Domain functions are adapted by default.
* Native functions don't require adaptation of their arguments.
* External functions are considered 'domain' functions by default.
Hence, their arguments are adapted, unless users of optimiser
exclude them from the process of automatic adaptation.
It can be done by registering them as 'native'.
AdaptRegistry can be safely used with multiprocessing
insofar as all relevant functions are registered as native
in the main process before child processes are started.
"""
_native_flag_attr_name_ = '_adapter_is_optimizer_native'
def __init__(self):
self._registered_native_callables = []
[docs] def register_native(self, fun: Callable) -> Callable:
"""Registers callable object as an internal function
that can work with internal graph representation.
Hence, it doesn't require adaptation when called by the optimiser.
Implementation details.
Works by setting a special attribute on the object.
This attribute then is checked by ``is_native`` used by adapters.
Args:
fun: function or callable to be registered as native
Returns:
Callable: same function with special private attribute set
"""
original_function = AdaptRegistry._get_underlying_func(fun)
setattr(original_function, AdaptRegistry._native_flag_attr_name_, True)
self._registered_native_callables.append(original_function)
return fun
[docs] def unregister_native(self, fun: Callable) -> Callable:
"""Unregisters callable object. See ``register_native``.
Args:
fun: function or callable to be unregistered as native
Returns:
Callable: same function with special private attribute unset
"""
original_function = AdaptRegistry._get_underlying_func(fun)
if hasattr(original_function, AdaptRegistry._native_flag_attr_name_):
delattr(original_function, AdaptRegistry._native_flag_attr_name_)
self._registered_native_callables.remove(original_function)
return fun
[docs] @staticmethod
def is_native(fun: Callable) -> bool:
"""Tests callable object for a presence of specific attribute
that tells that this function must not be restored with Adapter.
Args:
fun: tested Callable (function, method, functools.partial, or any callable object)
Returns:
bool: True if the callable was registered as native, False otherwise.
"""
original_function = AdaptRegistry._get_underlying_func(fun)
is_native = getattr(original_function, AdaptRegistry._native_flag_attr_name_, False)
return is_native
def clear_registered_callables(self):
# copy is to avoid removing elements from list while iterating
for f in copy(self._registered_native_callables):
self.unregister_native(f)
[docs] @staticmethod
def _get_underlying_func(obj: Callable) -> Callable:
"""Recursively unpacks 'partial' and 'method' objects to get underlying function.
Args:
obj: callable to try unpacking
Returns:
Callable: unpacked function that underlies the callable, or the unchanged object itself
"""
while True:
if isinstance(obj, partial): # if it is a 'partial'
obj = obj.func
elif hasattr(obj, '__func__'): # if it is a 'method'
obj = obj.__func__
else:
return obj # return the unpacked underlying function or the original object
[docs]def register_native(fun: Callable) -> Callable:
"""Out-of-class version of the ``register_native``
function that's intended to be used as a decorator.
Args:
fun: function or callable to be registered as native
Returns:
Callable: same function with special private attribute set
"""
return AdaptRegistry().register_native(fun)