Encontro 09: Simulação de Balanceamento

O enunciado da Escrita 3 continua ao longo deste notebook.

Preste atenção nas partes em negrito.

Importando a biblioteca:


In [ ]:
import sys
sys.path.append('..')

import socnet as sn

Configurando a biblioteca:


In [ ]:
sn.node_size = 3
sn.node_color = (0, 0, 0)

sn.edge_width = 1
sn.edge_color = (192, 192, 192)

Gerando um grafo completo:


In [ ]:
g = sn.generate_complete_graph(15)

sn.show_graph(g)

Esse será o grafo dos professores.

Atribuindo aleatoriamente tipos negativos e positivos às arestas:


In [ ]:
from random import random


def randomize_types(g):
    for n, m in g.edges():
        if random() < 0.5:
            g.edges[n, m]['type'] = -1
        else:
            g.edges[n, m]['type'] = 1


randomize_types(g)

Essa distribuição uniforme modela de forma aproximada os julgamentos dos professores após o "retreat". Não vamos usar uma atribuição fixa de tipos, pois sua solução não pode ser "viciada". Ela deve ser adequada para qualquer atribuição aleatória próxima.

Convertendo tipos em cores para visualização:


In [ ]:
def convert_types_to_colors(g):
    for n, m in g.edges():
        if g.edges[n, m]['type'] == -1:
            g.edges[n, m]['color'] = (255, 0, 0)
        else:
            g.edges[n, m]['color'] = (0, 0, 255)


convert_types_to_colors(g)

sn.show_graph(g)

Definindo uma atualização de tipo:


In [ ]:
from random import choice


# Essa função calcula a pressão que cada aresta sofre para mudar seu tipo,
# ou seja, a quantidade de situações de influência nas quais está envolvida.
# Se nenhuma aresta está sofrendo pressão, a função devolve False. Senão,
# uma aresta com maior pressão inverte seu tipo e a função devolve True.
#
# Os parâmetros a, b, c representam os pesos dos três tipos de pressão.
#
# a: peso da Situação A ("inimigo comum")
#
# b: peso da Situação B ("pacificador")
#
# c: peso da Situação C ("cizânia")
#
# Os valores padrão são 1. Já vimos em sala que eles levam a polarização.

def update_type(g, a=1, b=1, c=1):

    # Inicializa as pressões.

    for n, m in g.edges():
        g.edges[n, m]['pressure'] = 0

    # Para cada triângulo do grafo.

    for n in g.nodes():
        for m in g.nodes():
            if n != m:
                for l in g.nodes():
                    if n != l and m != l:

                        # Armazena em uma lista as três arestas do triângulo.

                        edges = [(n, m), (n, l), (m, l)]

                        # Conta quantas dessas arestas são positivas.

                        positives = 0

                        for e in edges:
                            if g.edges[e[0], e[1]]['type'] == 1:
                                positives += 1

                        # Se existem zero ou duas positivas, aumenta as pressões.

                        if positives == 0:
                            for e in edges:
                                g.edges[e[0], e[1]]['pressure'] += a # Situação A

                        if positives == 2:
                            for e in edges:
                                if g.edges[e[0], e[1]]['type'] == -1:
                                    g.edges[e[0], e[1]]['pressure'] += b # Situação B
                                else:
                                    g.edges[e[0], e[1]]['pressure'] += c # Situação C

    # Obtém a maior pressão.

    pressure = max([g.edges[n, m]['pressure'] for n, m in g.edges()])

    # Se essa maior pressão for zero, devolve False.

    if pressure == 0:
        return False

    # Senão, escolhe aleatoriamente uma aresta com maior pressão e inverte seu tipo.

    n, m = choice([(n, m) for n, m in g.edges() if g.edges[n, m]['pressure'] == pressure])

    g.edges[n, m]['type'] *= -1

    return True

Você não precisa modificar update_type, apenas decidir quais valores de a, b e c devem ser passados.

Definindo um contador de componentes:


In [ ]:
from queue import Queue


# Não é obrigatório entender esse código, mas eu ficarei triste se
# você não perceber que é apenas uma sequência de buscas em largura!

def count_components(g):
    for n in g.nodes():
        g.node[n]['label'] = 0

    label = 0
    q = Queue()

    for s in g.nodes():
        if g.node[s]['label'] == 0:
            label += 1

            g.node[s]['label'] = label
            q.put(s)

            while not q.empty():
                n = q.get()

                for m in g.neighbors(n):
                    if g.node[m]['label'] == 0 and g.edges[n, m]['type'] == 1:
                        g.node[m]['label'] = label
                        q.put(m)

    return label

Simulando várias vezes o processo no qual as arestas do grafo mudam de tipo até as pressões acabarem.

No final das simulações, o número médio de componentes é impresso.

Esse é o código que você deve modificar. Em particular, deve modificar as variáveis a, b e c.


In [ ]:
TIMES = 100
LIMIT = 100

a = 1 # Situação A
b = 1 # Situação B
c = 1 # Situação C

# Inicializa o número médio de componentes.
mean_components = 0

for _ in range(TIMES):

    # Inicializa as arestas para uma nova simulação.
    randomize_types(g)

    # Inicializa o contador de iterações.
    iterations = 0

    # Continua enquanto alguma aresta sofre pressão.
    while update_type(g, a=a, b=b, c=c):
        iterations += 1

        # Se passou de LIMIT iterações, provavelmente nunca vai terminar.
        if iterations == LIMIT:
            break

    # Se uma das simulações não convergiu, desiste.
    if iterations == LIMIT:
        break

    # Senão, atualiza o número médio de componentes.
    mean_components += count_components(g)

if iterations == LIMIT:
    print('uma das simulações não parecia estar convergindo')
else:
    # Finaliza o número médio de componentes
    mean_components /= TIMES

    print('número médio de componentes:', mean_components)

Para ter insights sobre o que está acontecendo, não esqueça de examinar a versão animada da simulação!


In [ ]:
def snapshot(g, frames):
    convert_types_to_colors(g)

    frame = sn.generate_frame(g)

    frames.append(frame)


frames = []

# Inicializa as arestas para uma nova simulação.
randomize_types(g)

# Inicializa aleatoriamente as posições dos nós.
sn.randomize_positions(g)

snapshot(g, frames)

iterations = 0

# Continua enquanto alguma aresta sofre pressão.
while update_type(g, a=a, b=b, c=c):

    # Move um pouco os vértices de posição, usando o atributo 'type' das
    # arestas como referência para saber o quanto dois vértices se atraem.
    sn.update_positions(g, 'type')

    snapshot(g, frames)

    iterations += 1

    # Se passou de LIMIT iterações, provavelmente nunca vai terminar.
    if iterations == LIMIT:
        print('a simulação não parecia estar convergindo')
        break

sn.show_animation(frames)

ATENÇÃO: Segundo a primeira parte do enunciado, uma das configurações que você deve encontrar é a que leva a polarização. Essa não pode ser a configuração na qual os três pesos são iguais, pois já foi vista em sala. Não se preocupe, existem outras!