Source code for mrfmsim.modifier

from mmodel.utility import param_sorter
from functools import wraps
import numba as nb
from inspect import Parameter, signature, Signature
from pprint import pformat
from copy import deepcopy
from collections import defaultdict
import mmodel


[docs] def replace_component(replacement: dict, allow_duplicate=False): """Modify the signature with components. The modifier modifies the internal model function. The wrapper function is keyword-only. The function by default does not allow duplicated signature. If we want to replace several attributes with a component, the component name cannot exist in the original signature. For example, if we have a function def func(a, b, obj): return a + b, obj and we want to replace a and b with a component, we cannot name the component "obj". In rares that we do want to replace a, b with a component, def func(obj): return obj.a + obj.b, obj func(obj=obj) we would have to use "obj_obj" as the component name. The result function is equivalent to def func(obj_obj, obj): return obj_obj.a + obj_obj.b + obj func(obj_obj=obj, obj=obj) The behavior is very confusing to users. The solution is to replace the components, but leave the original "obj" signature as it is. In the final signature, the duplicated signatures are combined into one. The solution here is to add a boolean flag allow_duplicate. If the flag is set to True, the function allows duplicated signatures. The solution, however, leaves another ambiguity. If we indeed want to replace the component with the same name, but use the original name with an attribute: def func(obj): return obj.a + obj.b, obj.obj func(obj=obj) We have decided that this behavior is not allowed regardless the flag. Because in this case, in an inspection attempt, it is confusing to understand if the obj is the original obj or an attribute to the new obj. In this case, a new object name should be used. :param dict[str] replacement: in the format of {component_object: [replacement_attribute1, replacement_attribute2, ...]} """ def modifier(func): sig = signature(func) params = sig.parameters # immutable new_params_dict = dict(params) # mutable replacement_dict = defaultdict(list) # in the event that the duplication is allowed # and the component is already in the signature # the list is maintained for the wrapped function duplicated_copmp = [] for comp, rep_attrs in replacement.items(): if not allow_duplicate: # check duplication last assert ( comp not in params ), f"parameter {repr(comp)} already in the signature" elif comp in params: duplicated_copmp.append(comp) # new_params_dict[comp] = Parameter(comp, 1) for attr in rep_attrs: # check attr # the error is related to the component dictionary # definition, regardless of the target function signature if attr == comp: raise ValueError( f"attribute name cannot be the same as component {repr(comp)}" ) if attr in params: replacement_dict[comp].append(attr) new_params_dict.pop(attr, None) # overwrite if duplicated new_params_dict[comp] = Parameter(comp, 1) @wraps(func) def wrapped(**kwargs): for comp, rep_attrs in replacement_dict.items(): if comp in duplicated_copmp: comp_obj = kwargs[comp] else: comp_obj = kwargs.pop(comp) for attr in rep_attrs: kwargs[attr] = getattr(comp_obj, attr) return func(**kwargs) wrapped.__signature__ = Signature( parameters=sorted(new_params_dict.values(), key=param_sorter) ) # deepcopy to prevent modification wrapped.param_replacements = deepcopy(replacement_dict) return wrapped modifier.metadata = f"replace_component({pformat(replacement)})" return modifier
[docs] def numba_jit(**kwargs): """Numba jit modifier with keyword arguments. Add metadata to numba.jit. The numba decorator outputs all the parameters make it hard to read. Use the decorator the same way as numba.jit(). """ @mmodel.modifier.add_modifier_metadata("numba_jit", **kwargs) def decorator(func): func = nb.jit(**kwargs)(func) return func return decorator