<jupyter_start><jupyter_text>Custom Networks Not only observer can be handcrafted with minimum work. You can also define custom networks with a very few small method declarations. Let's build a conventional boolean network with a small twist: The last node state is the majority voting of the rest of the nodes casted downwards in ties (Here we are completely ignoring the structure of the graph but this is only for educational purposes).<jupyter_code>from pybn.networks import AbstractNetwork
import numpy as np<jupyter_output><empty_output><jupyter_text>First of all we need to declare the constructor.<jupyter_code>class MyCustomNetwork(AbstractNetwork):
    #REQUIRED.
    def __init__(self, nodes, graph, awesome_parameter, bias=0.5, async_order=None):
        # Always wall the super constructor. It provides a few required steps to set up everything.
        super().__init__(nodes, graph, async_order=async_order)
        self.bias = bias

        # Super defines the state as a vector of integer but this may be change it needed.
        #self.state = np.zeros(nodes, dtype=int)
        
        # Super also defines a vector of empty Functions. Functions are created on-demand and represented as dictionaries.
        # This can also be redefine if needed.
        # self.functions = []
        # for _ in range(nodes):
        #     self.functions.append({})
        
        # You can also define custom parameters.
        self.my_awesome_iterable_parameter = awesome_parameter<jupyter_output><empty_output><jupyter_text>In some cases we will need to add some iterable parameter to our network. In order to work with our custom iterator one just need to include it in the from_configuration method under the parameters entry of the configuration dictionary.<jupyter_code>class MyCustomNetwork(MyCustomNetwork):
    #REQUIRED.
    @classmethod
    def from_configuration(cls, graph, configuration):
        """
        Create a Boolean Network from a configuration dictionary.
        """
        return cls(configuration['parameters']['nodes'], 
                   graph, 
                   configuration['parameters']['awesome_parameter'], 
                   configuration['parameters']['bias'])<jupyter_output><empty_output><jupyter_text>Since the state is not set in stone we need to tell the network how to create random states. In our case we just simply select a random vector of zeros and ones.<jupyter_code>class MyCustomNetwork(MyCustomNetwork):
    #REQUIRED.
    def random_state(self):
        return np.random.randint(0, 2, self.nodes)<jupyter_output><empty_output><jupyter_text>Finally we just need to tell the network how to it evolves. Node id is the internal identifier of the nodes. Here we just leave the conventional function creation but we will add a separate path if the node is the last one. Notice that rouding the average will do the trick for what we want to achive. Function input is an encoded version of the current state of the network that will serve as a key for the function dictionary.<jupyter_code>class MyCustomNetwork(MyCustomNetwork):
    #REQUIRED.
    def create_function_evaluation(self, function_input, node_id):
        if (node_id == self.nodes - 1):
            random_value = int(np.rint(np.mean(network.state[:-1])))
        else:
            random_value = 0 if np.random.rand() < self.bias else 1 
        self.functions[node_id][function_input] = random_value<jupyter_output><empty_output><jupyter_text>In some cases it may be neccesary to override the evaluate_function of the network. In general you will not even need to define it in the custom class and we will leave intact. We show it here to mention that the only important thing about this function is the output: it must be a valid node state of the custom network.<jupyter_code>class MyCustomNetwork(MyCustomNetwork):
    #OPTIONAL.
    def evaluate_function(self, node_id):
        # Compute function input.
        function_input = self.compute_function_input(node_id)
        # Evaluate function.
        if function_input not in self.functions[node_id]:
            self.create_function_evaluation(function_input, node_id)
        return self.functions[node_id][function_input]<jupyter_output><empty_output><jupyter_text>Testing the custom network Initialize the network.<jupyter_code>from pybn.graphs import uniform_graph

nodes = 8
average_connectivity = 2.4
graph = uniform_graph(nodes, average_connectivity)

awesome_parameter = np.pi
network = MyCustomNetwork(nodes, graph, awesome_parameter)<jupyter_output><empty_output><jupyter_text>Run the network a few steps.<jupyter_code># Set a random initial state.
steps = 5
network.set_initial_state()
print(network.state)
# Perform several steps.
for _ in range(steps):
    network.step()
    print(network.state)<jupyter_output>[0 1 1 0 1 0 0 1]
[1 1 1 1 0 0 0 0]
[1 1 1 1 1 1 0 1]
[1 1 1 1 0 1 0 1]
[0 0 1 1 0 1 0 1]
[1 0 1 1 0 0 0 1]
{'fraction_non_alphanumeric': 0.06853772625584706, 'fraction_numerical': 0.012202562538133009, 'mean_word_length': 3.831041257367387, 'pattern_counts': {'":': 0, '<': 33, '<?xml version=': 0, '>': 32, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '8', 'dir': 'jupyter-structured-clean-dedup', 'id': '440718', 'license': 'MIT', 'n_tokens_mistral': 1398, 'n_tokens_neox': 1329, 'n_words': 541, 'path': 'Tutorials/3_PyBN_Custom_Networks.ipynb', 'repo_name': 'Nogarx/PyBN', 'size': '4917'}