Huepe-Aldana noisy majority Boolean network
The Boolean network of Huepe and Aldana-González, nodes that follow the noisy majority of k inputs.
- Name:
noisy_boolean_network - Field:
biology/systems_biology - Formalism: discrete map
- References: Huepe and Aldana-González 2002; Gros 2015
Select this system
from skcomplex.systems import create_simulation, create_system, describe
system = create_system('noisy_boolean_network')
entry = describe('noisy_boolean_network')
trajectory = create_simulation('noisy_boolean_network').prepare().run().trajectory_
Factory
@register_system(
field='biology/systems_biology',
tags=('boolean network', 'noise', 'phase transition', 'mean field', 'stochastic map'),
references=('Huepe and Aldana-González 2002', 'Gros 2015'),
defaults={'until': 200, 'dt': 1},
)
def noisy_boolean_network(n: int = 1000, *, k: int = 3, eta: float = 0.1, seed: int | None = 0) -> System:
"""Return the Boolean network of Huepe and Aldana-González, nodes that follow the noisy majority of `k` inputs.
Every node takes the sign of the sum of its `k` random inputs, flipped with the probability `eta`. The order
parameter `Psi`, the time average of the absolute mean state, undergoes a continuous phase transition at the noise
`eta_c = (1 - 1 / Phi'(0)) / 2` of mean-field theory, with `Phi'(0) = k C(k - 1, (k - 1) / 2) / 2^(k - 1)`, which is
1/6 for `k = 3`. Below it the network orders, above it the mean state falls to fluctuations of order `1 /
sqrt(n)`.
Args:
n:
The number of nodes.
k:
The number of inputs of every node, odd.
eta:
The noise, the probability of a flip.
seed:
The seed of the inputs.
Returns:
system:
The system, with the states `nodes.sigma`.
Raises:
ValidationError:
The number of inputs is even.
Examples:
>>> from skcomplex.systems.biology.systems_biology import noisy_boolean_network
>>> noisy_boolean_network(10).elements[0].prepare().size_
10
"""
if k % 2 == 0:
error_msg = f'The number of inputs must be odd so that the majority is defined, not {k}.'
raise ValidationError(error_msg)
rng = np.random.default_rng(seed)
inputs = np.zeros((n, n))
for node in range(n):
inputs[node, rng.choice(np.delete(np.arange(n), node), size=k, replace=False)] = 1.0
nodes = GroupElement(
'nodes',
element=MajorityNodeElement,
size=n,
values={'sigma': np.ones(n), 'eta': eta},
)
majority = NoisyMajorityInteraction('majority', nodes='nodes', inputs=scipy.sparse.csr_array(inputs))
return System(elements=[nodes], interactions=[majority], time=DiscreteTime())
Components
Elements:
MajorityNodeElement, fromElement
Interactions:
NoisyMajorityInteraction, fromInteraction
class MajorityNodeElement(Element):
"""Node of a noisy Boolean network, a state of plus or minus one that follows the majority of its inputs.
Args:
label:
The label of the node or of the group of nodes.
visual:
How the element is drawn, or `None` for `default_visual_`.
sigma:
The state, -1 or 1.
eta:
The probability that the node takes the state opposite to the majority.
Examples:
>>> from skcomplex.systems.biology.systems_biology import MajorityNodeElement
>>> MajorityNodeElement('node', eta=0.1).eta
0.1
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = None,
*,
sigma: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([-1, 1]))] = 1.0,
eta: Annotated[float, ParameterSpec(domain=RealSet.closed(0, 1))] = 0.0,
) -> None:
super().__init__(label, visual=visual)
self.sigma = sigma
self.eta = eta
class NoisyMajorityInteraction(Interaction):
"""Update of every node of a Boolean network to the sign of the sum of its inputs, flipped with a probability.
Node `i` takes `sign(sum_j sigma_ij)` over its inputs with the probability `1 - eta` and the opposite sign with
the probability `eta`, all nodes at once, the network of Huepe and Aldana-González (2002).
Args:
label:
The label of the interaction.
nodes:
The path of the group of nodes.
inputs:
The input matrix, dense or sparse, one row per node with 1 for every input, an odd number per row.
Examples:
>>> from skcomplex.systems.biology.systems_biology import NoisyMajorityInteraction
>>> NoisyMajorityInteraction('majority', nodes='nodes', inputs=((0.0, 1.0), (1.0, 0.0))).nodes
'nodes'
"""
def __init__(
self,
label: str,
*,
nodes: Annotated[str, RoleSpec()],
inputs: Sequence[Sequence[float]] | scipy.sparse.sparray = ((0.0,),),
) -> None:
super().__init__(label)
self.nodes = nodes
self.inputs = inputs
def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
"""Return the noisy majority update.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
laws:
The laws of the component.
"""
node = s.nodes
weights = self.inputs if scipy.sparse.issparse(self.inputs) else np.asarray(self.inputs, dtype=float)
majority = where(total(node.sigma, weights=weights) > 0, 1, -1)
return [UpdateLaw(node.sigma, where(uniform('flip') < node.eta, -majority, majority))]
Equations
nodes.sigma[i]: sigma_nodes(t + 1) == where(uniform_flip_majority < eta_nodes, -where(_reduced_0 > 0, 1, -1), where(_reduced_0 > 0, 1, -1))