NNGT/doc/examples/attributes.py

172 lines
6.1 KiB
Python

# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2015-2023 Tanguy Fardet
# SPDX-License-Identifier: GPL-3.0-or-later
# doc/examples/attributes.py
''' Node and edge attributes '''
import numpy as np
import nngt
import nngt.generation as ng
''' -------------- #
# Generate a graph #
# -------------- '''
num_nodes = 1000
avg_deg = 25
graph = ng.erdos_renyi(nodes=num_nodes, avg_deg=avg_deg)
''' ----------------- #
# Add node attributes #
# ----------------- '''
# Let's make a network of animals where nodes represent either cats or dogs.
# (no discrimination against cats or dogs was intended, no animals were harmed
# while writing or running this code)
animals = ["cat" for _ in range(600)] # 600 cats
animals += ["dog" for _ in range(400)] # and 400 dogs
np.random.shuffle(animals) # which we assign randomly to the nodes
graph.new_node_attribute("animal", value_type="string", values=animals)
# Let's check the type of the first six animals
print(graph.get_node_attributes([0, 1, 2, 3, 4, 5], "animal"))
# Nodes can have attributes of multiple types, let's add a size to our animals
catsizes = np.random.normal(50, 5, 600) # cats around 50 cm
dogsizes = np.random.normal(80, 10, 400) # dogs around 80 cm
# We first create the attribute without values (for "double", default to NaN)
graph.new_node_attribute("size", value_type="double")
# We now have to attributes: one containing strings, the other numbers (double)
print(graph.node_attributes)
# get the cats and set their sizes
cats = graph.get_nodes(attribute="animal", value="cat")
graph.set_node_attribute("size", values=catsizes, nodes=cats)
# We set 600 values so there are 400 NaNs left
assert np.sum(np.isnan(graph.get_node_attributes(name="size"))) == 400, \
"There were not 400 NaNs as predicted."
# None of the NaN values belongs to a cat
assert not np.any(np.isnan(graph.get_node_attributes(cats, name="size"))), \
"Got some cats with NaN size! :'("
# get the dogs and set their sizes
dogs = graph.get_nodes(attribute="animal", value="dog")
graph.set_node_attribute("size", values=dogsizes, nodes=dogs)
# Some of the animals are part of human househols, they have therefore "owners"
# which will be represented here through a Human class.
# Animals without an owner will have an empty list as attribute.
class Human:
def __init__(self, name):
self.name = name
def __repr__(self):
return "Human<{}>".format(self.name)
# John owns all animals between 8 and 48
John = Human("John")
animals = [i for i in range(8, 49)]
graph.new_node_attribute("owners", value_type="object", val=[])
graph.set_node_attribute("owners", val=[John], nodes=animals)
# Now suppose another human, Julie, owns all animals between 0 and 40
Julie = Human("Julie")
animals = [i for i in range(0, 41)]
# to update the values, we need to get them to add Bob to the list
owners = graph.get_node_attributes(name="owners", nodes=animals)
for interactions in owners:
interactions.append(Julie)
graph.set_node_attribute("owners", values=owners, nodes=animals)
# now some of the initial owners should have had their attributes updated
new_owners = graph.get_node_attributes(name="owners")
print("There are animals owned only by", new_owners[0], "others owned only by",
new_owners[48], "and some more owned by both", new_owners[40])
''' ---------- #
# Edge weights #
# ---------- '''
# Same as for node attributes, one can give attributes to the edges
# Let's give weights to the edges depending on how often the animals interact!
# cat's interact a lot among themselves, so we'll give them high weights
cat_edges = graph.get_edges(source_node=cats, target_node=cats)
# check that these are indeed only between cats
cat_set = set(cats)
node_set = set(np.unique(cat_edges))
assert cat_set == node_set, "Damned, something wrong happened to the cats!"
# uniform distribution of weights between 30 and 50
graph.set_weights(elist=cat_edges, distribution="uniform",
parameters={"lower": 30, "upper": 50})
# dogs have less occasions to interact except some which spend a lot of time
# together, so we use a lognormal distribution
dog_edges = graph.get_edges(source_node=dogs, target_node=dogs)
graph.set_weights(elist=dog_edges, distribution="lognormal",
parameters={"position": 2.2, "scale": 0.5})
# Cats do not like dogs, so we set their weights to -5
# Dogs like chasing cats but do not like them much either so we let the default
# value of 1
cd_edges = graph.get_edges(source_node=cats, target_node=dogs)
graph.set_weights(elist=cd_edges, distribution="constant",
parameters={"value": -5})
# Let's check the distribution (you should clearly see 4 separate shapes)
if nngt.get_config("with_plot"):
nngt.plot.edge_attributes_distribution(graph, "weight")
''' ------------------- #
# Other edge attributes #
# ------------------- '''
# non-default edge attributes can be created as the node attributes
# let's create a class for humans and store it when two animals have interacted
# with the same human (the default will be an empty list if they did not)
# Alice interacted with all animals between 8 and 48
Alice = Human("Alice")
animals = [i for i in range(8, 49)]
edges = graph.get_edges(source_node=animals, target_node=animals)
graph.new_edge_attribute("common_interaction", value_type="object", val=[])
graph.set_edge_attribute("common_interaction", val=[Alice], edges=edges)
# Now suppose another human, Bob, interacted with all animals between 0 and 40
Bob = Human("Bob")
animals = [i for i in range(0, 41)]
edges2 = graph.get_edges(source_node=animals, target_node=animals)
# to update the values, we need to get them to add Bob to the list
ci = graph.get_edge_attributes(name="common_interaction", edges=edges2)
for interactions in ci:
interactions.append(Bob)
graph.set_edge_attribute("common_interaction", values=ci, edges=edges2)
# now some of the initial `edges` should have had their attributes updated
new_ci = graph.get_edge_attributes(name="common_interaction", edges=edges)
print(np.sum([0 if len(interaction) < 2 else 1 for interaction in new_ci]),
"interactions have been updated among the", len(edges), "from Alice.")