Skip to content

Commit

Permalink
Refactor some network generation functions
Browse files Browse the repository at this point in the history
- Refactor `inputs` into several functions
- Rename functions so their purpose is clearer
- Move `sigmoid()` to `utils`
- Make Ising function signatures more consistent
- Update sigmoid plotting functions in `ising`
  • Loading branch information
wmayner committed Jan 17, 2023
1 parent 6b3298f commit 9ebeb52
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 42 deletions.
24 changes: 8 additions & 16 deletions pyphi/network_generator/ising.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,9 @@
from . import utils


def sigmoid(E, T):
return 1 / (1 + np.exp(-E / T))


def inverse_sigmoid(p, sum_w, field):
return np.log(p / (1 - p)) / (sum_w - field)


def energy(element, weights, state, field):
def energy(element, weights, state):
"""Return the energy associated with the given spin."""
return utils.input_weight(element, weights, state) + field
return utils.total_weighted_input(element, weights, state)


@curry
Expand All @@ -44,7 +36,7 @@ def probability(

state = utils.binary2spin(state)
E = energy(element, weights, state, field)
return sigmoid(E, temperature)
return utils.sigmoid(E, temperature=temperature, field=field)


###############################################################################
Expand All @@ -56,8 +48,8 @@ def probability(
import pandas as pd
import seaborn as sb

def plot_sigmoid(x, temperature):
y = sigmoid(x, temperature)
def plot_sigmoid(x, temperature=1.0, field=0.0):
y = utils.sigmoid(x, temperature=temperature, field=field)
ax = sb.lineplot(x=x, y=y, linewidth=3)
ax.set_title(f"T = {temperature}")
ax.vlines(x=0, ymin=0, ymax=1, color="grey", linewidth=1)
Expand Down Expand Up @@ -86,9 +78,9 @@ def plot(weights, temperature, field, N=None, spin=0):
for state in states:
spin_state = utils.binary2spin(state)
# Compute probability that i'th spin is "on" in the next micro-timestep
E = energy(spin, weights, spin_state, field)
E = energy(spin, weights, spin_state, temperature=temperature, field=field)
energies.append(E)
probabilities.append(sigmoid(E, temperature))
probabilities.append(utils.sigmoid(E, temperature=temperature, field=field))

data = pd.DataFrame(
{
Expand All @@ -102,7 +94,7 @@ def plot(weights, temperature, field, N=None, spin=0):
x = np.linspace(-limit, limit, num=200)

fig = plt.figure(figsize=(15, 6))
ax = plot_sigmoid(x, temperature=temperature)
ax = plot_sigmoid(x, temperature=temperature, field=field)
ax = plot_inputs(
data=data, x="energy", y="probability", label="state", ax=ax, sep=0.05
)
Expand Down
12 changes: 6 additions & 6 deletions pyphi/network_generator/unit_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@


def logical_or_function(element, weights, state):
return utils.input_weight(element, weights, state) >= 1
return utils.total_weighted_input(element, weights, state) >= 1


def logical_and_function(element, weights, state):
# Convention: i,j means i -> j
num_inputs = (weights[:, element] > 0).sum()
return utils.input_weight(element, weights, state) >= num_inputs
return utils.total_weighted_input(element, weights, state) >= num_inputs


def logical_parity_function(element, weights, state):
return utils.input_weight(element, weights, state) % 2 >= 1
return utils.total_weighted_input(element, weights, state) % 2 >= 1


def logical_nor_function(element, weights, state):
Expand All @@ -35,7 +35,7 @@ def logical_nparity_function(element, weights, state):

@curry
def naka_rushton(element, weights, state, exponent=2.0, threshold=1.0):
x = utils.input_weight(element, weights, state) ** exponent
x = utils.total_weighted_input(element, weights, state) ** exponent
return x / (x + threshold)


Expand All @@ -59,7 +59,7 @@ def boolean_function(element, weights, state, on_inputs=()):
if len(set(map(len, on_inputs))) != 1:
raise ValueError("on_inputs must all be the same length")

inputs = tuple(utils.inputs(element, weights, state))
inputs = tuple(utils.weighted_inputs(element, weights, state))

if len(inputs) != len(next(iter(on_inputs), len(inputs))):
raise ValueError("nonzero input weights and on_input lengths must match")
Expand All @@ -80,5 +80,5 @@ def gaussian(
sigma=0.5,
):
state = utils.binary2spin(state)
x = utils.input_weight(element, weights, state)
x = utils.total_weighted_input(element, weights, state)
return gauss(x, mu, sigma)
65 changes: 45 additions & 20 deletions pyphi/network_generator/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,30 @@
import numpy as np


def input_weight(element, weights, state):
"""Return the amount of input weight being sent to the given element."""
return np.dot(state, weights[:, element])
def weighted_inputs(element, weights, state):
weights, state = inputs(element, weights, state)
return weights * state


def inputs(element, weights, state, ordering="topological", layers=None):
"""Return the inputs being sent to the given element.
Inputs are returned in the order specified by `ordering`.
Topological ordering rotates the indices so that the element is first.
"""
state = np.array(state)
_input_weights = input_weights(element, weights)
if layers is None:
layers = [list(range(weights.shape[0]))]
if ordering == "topological":
_input_weights, state = to_topological_ordering(
element, _input_weights, state, layers
)
idx = np.nonzero(_input_weights)
return _input_weights[idx], state[idx]


def to_topological_ordering(element, input_weights, state, layers):
def to_topological_ordering(element, weights, state, layers):
topo_input_weights = []
topo_state = []
layer_sizes = set()
Expand All @@ -20,29 +38,36 @@ def to_topological_ordering(element, input_weights, state, layers):
"cannot use topological ordering with different layer sizes"
)
layer = sorted(layer)
layer_input_weights = input_weights[layer]
layer_input_weights = weights[layer]
layer_state = state[layer]
topo_input_weights.extend(np.roll(layer_input_weights, -element))
topo_state.extend(np.roll(layer_state, -element))
return np.array(topo_input_weights), np.array(topo_state)


def inputs(element, weights, state, ordering="topological", layers=None):
"""Return the inputs being sent to the given element.
def total_weighted_input(element, weights, state):
"""Return the amount of weighted input being sent to the given element."""
return np.dot(state, weights[:, element])

Inputs are returned in the order specified by `ordering`.
Topological ordering rotates the indices so that the element is first.
"""
state = np.array(state)
input_weights = weights[:, element]
if layers is None:
layers = [list(range(weights.shape[0]))]
if ordering == "topological":
input_weights, state = to_topological_ordering(
element, input_weights, state, layers
)
idx = input_weights > 0
return input_weights[idx] * np.array(state)[idx]

def total_input_weight(element, weights):
"""Return the sum of connection weights being sent to the given element."""
return np.sum(weights[:, element])


def input_weights(element, weights):
"""Return the connection weights being sent to the given element."""
return weights[:, element]


def sigmoid(energy, temperature=1.0, field=0.0):
"""The logistic function."""
return 1 / (1 + np.exp(-(energy - field) / temperature))


def inverse_sigmoid(p, sum_w, field):
"""The inverse of the logistic function."""
return np.log(p / (1 - p)) / (sum_w - field)


def binary2spin(binary_state):
Expand Down

0 comments on commit 9ebeb52

Please sign in to comment.