# substrate_generator/mechanism_combinations.py
"""Combination strategies for composite units.
A composite unit holds several sub-mechanisms and combines their activation
probabilities into one. In the present=past substrate TPM every sub-mechanism is
evaluated at the same from-state, so a combination is simply a function of the
list of sub-mechanism probabilities.
:func:`composite` builds a unit function from a list of sub-mechanism specs and a
combination strategy.
"""
import numpy as np
from .mechanisms import MECHANISMS
[docs]
def selective(probs, **kwargs):
"""The sub-mechanism probability farthest from 0.5 (the most decisive)."""
probs = np.asarray(probs, dtype=float)
return float(probs[np.argmax(np.abs(probs - 0.5))])
[docs]
def average(probs, **kwargs):
"""The mean of the sub-mechanism probabilities."""
return float(np.mean(probs))
[docs]
def maximal(probs, **kwargs):
"""The maximum sub-mechanism probability."""
return float(np.max(probs))
[docs]
def first_necessary(probs, *, steepness=5.0, offset=0.5, **kwargs):
"""The first (primary) sub-mechanism, boosted toward 1 when the others are
inactive — but only if the primary is already above 0.5.
``steepness`` and ``offset`` parameterize the logistic boost; they default
to 5 and 0.5.
"""
primary = probs[0]
if primary > 0.5:
non_primary = np.prod([1 - p for p in probs[1:]])
max_boost = 1 - primary
boost = max_boost / (1 + np.e ** (-steepness * (1 - non_primary - offset)))
return float(primary + boost)
return float(primary)
[docs]
def integrator(probs, **kwargs):
"""The sum of the sub-mechanism probabilities, clamped to [0, 1]."""
return float(min(1.0, max(0.0, np.sum(probs))))
[docs]
def serial(probs, **kwargs):
"""Series combination: ``1 - prod(1 - p)`` over the sub-mechanisms."""
remainder = 1.0
for p in probs:
remainder -= p * remainder
return float(1 - remainder)
MECHANISM_COMBINATIONS = {
"selective": selective,
"average": average,
"maximal": maximal,
"first_necessary": first_necessary,
"integrator": integrator,
"serial": serial,
}
[docs]
def composite(sub_specs, combination="selective", **combination_kwargs):
"""Build a composite unit function from sub-mechanism specs.
Parameters
----------
sub_specs : Iterable[Mapping]
An iterable of dicts, each ``{"mechanism": name | callable,
"inputs": tuple[int] | None, "params": dict}``. Each sub-mechanism is
evaluated at the full from-state and reads its own ``inputs``.
combination : str or Callable, optional
A name in :data:`MECHANISM_COMBINATIONS` or a callable mapping a
sequence of probabilities to a single probability. Defaults to
``"selective"``.
Other Parameters
----------------
**combination_kwargs
Passed to the combination strategy (e.g. ``steepness`` / ``offset`` for
``first_necessary``).
Returns
-------
Callable
A unit function ``f(element, weights, state, **kwargs) -> float`` whose
value is the combined probability of its sub-mechanisms.
Raises
------
TypeError
If a sub-spec has a key other than ``mechanism``, ``inputs`` and
``params``, or ``params`` or ``combination_kwargs`` names an option
the mechanism or combination does not take. A callable that accepts
any keyword is not checked.
"""
# Imported here: the package imports this module.
from . import _check_spec
from . import _named_options
from . import _reject_unknown
if isinstance(combination, str):
combine = MECHANISM_COMBINATIONS[combination]
else:
combine = combination
options = _named_options(combine, 1)
if options is not None:
_reject_unknown(combination_kwargs, options, "the combination's options")
sub_specs = list(sub_specs)
for k, spec in enumerate(sub_specs):
_check_spec(spec, f"sub-spec {k}")
resolved = []
for spec in sub_specs:
mech = spec["mechanism"]
func = MECHANISMS[mech] if isinstance(mech, str) else mech
inputs = spec.get("inputs")
inputs = tuple(inputs) if inputs is not None else None
resolved.append((func, inputs, dict(spec.get("params", {}))))
def composite_func(element, weights, state, **kwargs):
probs = [
func(element, weights, state, inputs=inputs, **params)
for func, inputs, params in resolved
]
return combine(probs, **combination_kwargs)
return composite_func