"""
# q-Voter
## A warm-up exercise in networkx
First of all, we import the networkx library, which is the basic python library for network theory.
"""
import networkx as nx
import numpy as np
from matplotlib import figure
import matplotlib.pyplot as plt
import random
"""
The first thing we need to do is to create a fully connected network with N nodes (individuals). Each one of these individuals is described from the binary variables $S=\pm1$.

We choose to create a network with 20 nodes. Each of these nodes is linked with all the others. So the length of the network is the number of its nodes.
"""
G = nx.complete_graph(20)
len(G)
"""
The size of the network is the number of its connections.
"""
G.size()
"""
The next step we must do is the network vizualization. As we can see, we have a basic problem: the number are not binary. We must do something and change them.
"""
nx.draw(G, with_labels=True, font_weight='bold')
"""
## Building a network with voters
The main problem we had before was that even if we created the network, we didn't succeed to put labels. How we can solve this problem?

We create an empty network. In our elections there are two canditates: (+1) and (-1). Initially, our voters they choose randomly one of these candidates.
"""
# Number of nodes
Num_nodes = 30

# Create graph
H = nx.Graph()

votes = [+1,-1]

# Create the nodes
for i in range(Num_nodes):
    H.add_node(i, vote=random.choice(votes))
"""
The next thing we can do is to color the nodes of our network depending on the voting preference of our individuals.
"""
# Initialize nodes randomly and put different colors to each vote
color_map = []
for node, data in H.nodes(data=True):
    if data['vote'] == +1:
        color_map.append(0.25)  # blue color
    elif data['vote'] == -1:
        color_map.append(0.9)  # red color

# Draw the Graph
nx.draw(H, vmin=0, vmax=1, cmap=plt.cm.jet, node_color=color_map, with_labels=True)
plt.show()
"""
The only problem we have now is that our network is not connected. Basically, we want it to be fully connected. So we can connect all the nodes in one loop
"""
# Add one and only one connection for each pair of nodes
for i in range(len(H)):
    for j in range(i+1,len(H)):
        H.add_edge(i,j)
"""
And then we can plot our graph to see that all we did was correct!
"""
nx.draw(H, vmin=0, vmax=1, cmap=plt.cm.jet, node_color=color_map, with_labels=True)
plt.show()
"""
## Iterating our q-Voter model
Previously, we created a graph with $N$ individuals, in which all the individuals were connected. We create a random variable $S_{i}=\pm 1$.

At each step of our iteration we choose randomly q-individuals/voters/nodes, who form a group of influence, called q-lobby. Then we choose randomly another one individual that the group can influence.

Now if all the members of the group have the same opinion (same state), then our voter changes his mind. If they don't have an unanimous opinion, still the voter can flip with a probability $\epsilon$.
"""
def repaint(H):
    '''
    It takes a network and it paints its nodes with different colors dependent on their votes
    
    Input:
    - H (network): our network
    
    Output:
    - color_map: The new artwork of our labels
    '''
    
    # Initialize nodes randomly and put different colors to each vote
    color_map = []
    for node, data in H.nodes(data=True):
        if data['vote'] == +1:
            color_map.append(0.25)  # blue color
        elif data['vote'] == -1:
            color_map.append(0.9)  # red color
            
    return color_map
# Initialization
Nsteps = 10000
q = 7
viz_step = 1000
epsilon = 0.25

Nplus = 0
    
for node, data in H.nodes(data=True):
    if data['vote'] == +1:
        Nplus = Nplus + 1

c = Nplus / H.number_of_nodes()

c_list=[c]

# An array with all the possible nodes
Nodes = list(range(1,Num_nodes))

# Simulation loop
for i in range(Nsteps):
    qlobby = random.sample(Nodes, q)
    voter = random.sample(Nodes, 1)
    
    # sum all votes in lobby
    sum_votes = 0
    for qs in qlobby:
        sum_votes = sum_votes + H.nodes[qs]['vote']
       
    # If all the votes in lobby were the same, the voter changes his mind,
    # If all the votes are not the same, our voter flips with probability -1
    if abs(sum_votes) == q:
        H.nodes[voter[0]]['vote'] = H.nodes[qs]['vote']
    elif random.uniform(0, 1) < epsilon:
        H.nodes[voter[0]]['vote'] = -H.nodes[voter[0]]['vote']
        
    Nplus = 0
    
    for node, data in H.nodes(data=True):
        if data['vote'] == +1:
            Nplus = Nplus + 1

    c = Nplus / H.number_of_nodes()
    
    c_list.append(c)
    
    # show a snapshot of our system for every viz_step step
    if i % viz_step == 0:
        color_map = repaint(H)
        nx.draw(H, vmin=0, vmax=1, cmap=plt.cm.jet, node_color=color_map, with_labels=True)
        plt.show()
        
plt.figure(figsize=(20, 3))
plt.plot(np.arange(len(c_list)),c_list,'r-')
plt.xlabel('Monte Carlo Steps')
plt.ylabel(r'$c=N_{+}/N$')
plt.title(r'steps={},$N=${},$\epsilon=${},q={}'.format(Nsteps,Num_nodes,epsilon,q))
plt.show()
"""
# q-Voter with independence model function

Here we create a function that applies the q-Voter model with independence.

The algorithm is described on the paper "Phase transitions in the q-voter model with two types of stochastic driving" of Piotr Nyczka, Katarzyna Sznajd-Weron, and Jerzy Cisło. With probability $p$ the voter behaves independently and with $1-p$ like a conformist. In the case of the independent behavior an individual changes to the opposite state with probability $1/2$.
"""
def compute_c(H):
    '''
    Inputs:
    
    H (graph): our graph
    
    Output:
    
    c (float): the fraction N+/N
    '''
    Nplus = 0
    
    for node, data in H.nodes(data=True):
        if data['vote'] == +1:
            Nplus = Nplus + 1
    
    c = Nplus / H.number_of_nodes()
    
    return c

def qVoter_indep(Num_nodes=200,Nsteps=100000, q=7, p=0.2,MC_step=1000, viz=False):
    '''
    Inputs:
    
    - Num_nodes (int): The number of our network nodes
    - Nsteps (int): The number of steps of our integration
    - q (int): The number of loggy's members
    - p (float): The probability of choosing between independence and conformity
    - MC_step (int): the step for which we compute the concentration of our system
    - viz (logical): True if you want the plot of time dependence of c(t) and the final snapshot of the system
    
    Outputs:
    - A vizualization of the network in the last state
    - The time evolution of c(t)
    - c_list (list): a list with the values of c(t) for each MC step 
    '''
    # Create graph
    H = nx.Graph()
    
    # The possible votes
    votes = [+1,-1]
    
    # Create the nodes
    for i in range(Num_nodes):
        H.add_node(i, vote=random.choice(votes))
        
    # Initialize nodes randomly and put different colors to each vote
    color_map = repaint(H)
            
    # Add one and only one connection for each pair of nodes
    for i in range(len(H)):
        for j in range(i+1,len(H)):
            H.add_edge(i,j)

    # An array with all the possible nodes
    Nodes = list(range(1,Num_nodes))
    
    # A list with all cs
    c_list = []
    c_list.append(compute_c(H))
    
    # Simulation loop
    for i in range(Nsteps):
        # choose randomly a voter and a q-lobby
        voter = random.sample(Nodes, 1)
        new_list = Nodes[0:voter[0]]+Nodes[voter[0]+1:len(Nodes)] # the voter can't be in the q-lobby
        qlobby = random.sample(new_list, q)
        
        # sum all votes in lobby
        sum_votes = 0
        for qs in qlobby:
            sum_votes = sum_votes + H.nodes[qs]['vote']
        
        # with probability p, the voter behaves independently
        # with probability 1-p behaves like a conformist
        if random.uniform(0, 1)<p:
            if random.uniform(0, 1)<0.5:
                H.nodes[voter[0]]['vote'] = -H.nodes[voter[0]]['vote']
        else:
            if abs(sum_votes) == q:
                H.nodes[voter[0]]['vote'] = H.nodes[qs]['vote']
            
        # show a snapshot of our system for every viz_step step
        if i % MC_step == 0:
            color_map = repaint(H)
            
            # Compute c
            c_list.append(compute_c(H))
            
    if viz:        
        nx.draw(H, vmin=0, vmax=1, cmap=plt.cm.jet, node_color=color_map, with_labels=True)
        plt.title('Network for t={}'.format(i), fontsize=18)
        plt.show()

        plt.figure(figsize=(20, 3))
        plt.plot(np.arange(len(c_list)),c_list,'r-')
        plt.xlabel('Monte Carlo Steps')
        plt.ylabel(r'$c=N_{+}/N$')
        plt.title(r'steps={},$N=${},$q=${}, $p=${}, MC_step={}'.format(Nsteps,Num_nodes,q,p,MC_step))
        plt.show()
    
    return c_list
c = qVoter_indep(Num_nodes=200, Nsteps=5000000, q=7, p=0.111, MC_step=100, viz=True)
c = qVoter_indep(Num_nodes=200, Nsteps=5000000, q=5, p=0.175, MC_step=100, viz=True)
"""
As we can see the phase transitions are clear from these diagrams. In the first diagram we have the coexistence of three states, whereas in the last diagram we have the coexistence of two states.
"""
"""
# Biffurcation Diagrams
From the previous two diagrams it is clear that a phase transition occurs in our system. To have a clearer picture of our phase transition, we must plot the bifurcation diagrams of our systems, which show us $c(t)$ as a function of $p$.

Of course this will need more computational time due to the fact that we must run a Monte carlo simulation for each $p$. However, we can start from easy analytical results.

## The Analytical Way

The easiest way to extract this diagram is by assuming stationary state

\begin{equation}
\dfrac{\partial c}{\partial t}=0 \;\wedge\; \dfrac{\partial b}{\partial t}=0
\end{equation}

And then find find the formula for $p$,

\begin{equation}
p=\dfrac{c_{-}\theta^{q}_{-}-c_{+}\theta^{q}_{+}}{c_{-}\theta^{q}_{-}-c_{+}\theta^{q}_{+}-(c_{-}-c_{+})/2}
\end{equation}

where

\begin{align}
P(+)&=c_{+}=c\\
P(-)&=c_{-}=1-c
\end{align}

and

\begin{align}
\theta_{+}&=P(-|+)=\dfrac{P(-+)}{P(+)}=\dfrac{b}{2c}\\
\theta_{-}&=P(+|-)=\dfrac{P(+-)}{P(-)}=\dfrac{b}{2(1-c)}
\end{align}

where $b=P(+-)+P(-+)$ or $P(+-)=P(-+)=b/2$.

We have almost everything to find the depndence of concentration $c$ from $p$. The only problem we have is that we need the equation of stationary state for $b$. And this equation is the following,

\begin{equation}
b_{\mathrm{st}}=2 \frac{c_{\mathrm{st}}\left(1-c_{\mathrm{st}}\right)\left[\left(1-c_{\mathrm{st}}\right)^{q}-c_{\mathrm{st}}^{q}\right]-\frac{q}{\langle k\rangle}\left(1-2 c_{\mathrm{st}}\right)\left[c_{\mathrm{st}}\left(1-c_{\mathrm{st}}\right)^{q}+\left(1-c_{\mathrm{st}}\right) c_{\mathrm{st}}^{q}\right]}{\left(1-c_{\mathrm{st}}\right)^{q}-c_{\mathrm{st}}^{q}-\frac{q}{\langle k\rangle}\left(1-2 c_{\mathrm{st}}\right)\left[\left(1-c_{\mathrm{st}}\right)^{q}+c_{\mathrm{st}}^{q}\right]}
\end{equation}

where $<k>$ is the degree of our network. Here we work with a complete network, which means that each node is connected with all the others. So $<k>=N-1$.

Therefore, we define our functions,
"""
def bst(c,k,q):
    num = c*(1-c)*((1-c)**q-c**q)-q/k*(1-2*c)*(c*(1-c)**q+(1-c)*c**q)
    den = (1-c)**q-c**q-q/k*(1-2*c)*((1-c)**q+c**q)
    b = 2*num/den
    return b

def th_plus(c,k,q):
    return bst(c,k,q)/(2*c)

def th_min(c,k,q):
    return bst(c,k,q)/(2*(1-c))

def c_plus(c):
    return c

def c_min(c):
    return 1-c

def p(c,k,q):
    num = c_min(c)*th_min(c,k,q)**q-c_plus(c)*th_plus(c,k,q)**q
    den = c_min(c)*th_min(c,k,q)**q-c_plus(c)*th_plus(c,k,q)**q - (c_min(c)-c_plus(c))/2
    return num/den
"""
And then we can create easily the plots of biffurcation diagrams by inverting the previous equations.
"""
c = np.linspace(0.1,0.9,1000)
q = 7
k_range = np.arange(20,45,5)

plt.figure(figsize=(7, 5))

for k in k_range:
    plt.plot(p(c=c,k=k,q=q),c,'--',label=r'<k>={}'.format(k))
    
plt.xlabel(r'$p$')
plt.ylabel(r'$c=N_{+}/N$')
plt.grid()
plt.title(r'Bifurcation Diagram for q={}'.format(q))
plt.legend()

zeros1 = np.linspace(0.01,0.06,1000)
plt.plot(zeros1,0.5*np.ones(len(zeros1)),'k--')
zeros2 = np.linspace(0.06,0.13,1000)
plt.plot(zeros2,0.5*np.ones(len(zeros2)),'k-')

c = np.linspace(0.01,0.15,1000)
for k in k_range:
    plt.plot(p(c=c,k=k,q=q),c,'b-')
    
c = np.linspace(0.85,0.99,1000)
for k in k_range:
    plt.plot(p(c=c,k=k,q=q),c,'b-')

plt.show()
c = np.linspace(0.01,0.99,1000)
q = 2
k_range = np.arange(10,45,5)

plt.figure(figsize=(7, 5))

for k in k_range:    
    plt.plot(p(c=c,k=k,q=q),c,'-',label=r'<k>={}'.format(k))
    
plt.xlabel(r'$p$')
plt.ylabel(r'$c=N_{+}/N$')
plt.grid()
plt.title(r'Bifurcation Diagram for q={}'.format(q))
plt.legend()
zeros1 = np.linspace(0.01,0.28,1000)
plt.plot(zeros1,0.5*np.ones(len(zeros1)),'k--')
zeros2 = np.linspace(0.28,0.4,1000)
plt.plot(zeros2,0.5*np.ones(len(zeros2)),'k-')
plt.show()
"""
Here we also create bifurcation diagrams for $<k>=199$, which is the mean node degree of our system.
"""
c = np.linspace(0.1,0.9,1000)
q = 7
k_range = [199]

plt.figure(figsize=(7, 5))

for k in k_range:
    plt.plot(p(c=c,k=k,q=q),c,'--',label=r'<k>={}'.format(k))
    
plt.xlabel(r'$p$')
plt.ylabel(r'$c=N_{+}/N$')
plt.grid()
plt.title(r'Bifurcation Diagram for q={}'.format(q))
plt.legend()

zeros1 = np.linspace(0.01,0.083,1000)
plt.plot(zeros1,0.5*np.ones(len(zeros1)),'k--')
zeros2 = np.linspace(0.083,0.13,1000)
plt.plot(zeros2,0.5*np.ones(len(zeros2)),'k-')

c = np.linspace(0.01,0.15,1000)
for k in k_range:
    plt.plot(p(c=c,k=k,q=q),c,'b-')
    
c = np.linspace(0.85,0.99,1000)
for k in k_range:
    plt.plot(p(c=c,k=k,q=q),c,'b-')

plt.show()
c = np.linspace(0.01,0.99,1000)
q = 2
k_range = [199]

plt.figure(figsize=(7, 5))

for k in k_range:    
    plt.plot(p(c=c,k=k,q=q),c,'-',label=r'<k>={}'.format(k))
    
plt.xlabel(r'$p$')
plt.ylabel(r'$c=N_{+}/N$')
plt.grid()
plt.title(r'Bifurcation Diagram for q={}'.format(q))
plt.legend()
zeros1 = np.linspace(0.01,0.33,1000)
plt.plot(zeros1,0.5*np.ones(len(zeros1)),'k--')
zeros2 = np.linspace(0.33,0.4,1000)
plt.plot(zeros2,0.5*np.ones(len(zeros2)),'k-')
plt.show()
"""
## The Computational Way
Here we will run a Monte Carlo Simulation for each different value of $p$. This takes more time, but it is the best way to make sure that our results are valid and make sense.

Our method is to keep the last point of each Monte Carlo simulation and then plot all points versus $p$.
"""
p_range = np.linspace(0.01,0.2,500)
c_list = []

for p in p_range:
    c = qVoter_indep(Num_nodes=200, Nsteps=1000000, q=7, p=p, MC_step=10000, viz=False)
    c_list.append(c[-1])

plt.plot(p_range,c_list,'ko')
zeros1 = np.linspace(0.01,0.2,1000)
plt.plot(zeros1,0.5*np.ones(len(zeros1)),'k--')
plt.xlabel(r'$p$')
plt.ylabel(r'$c=N_{+}/N$')
plt.grid()
plt.title(r'Bifurcation Diagram for q=7')
plt.legend()
p_range = np.linspace(0.01,0.6,500)
c_list = []

for p in p_range:
    c = qVoter_indep(Num_nodes=200, Nsteps=1000000, q=2, p=p, MC_step=10000, viz=False)
    c_list.append(c[-1])
    
plt.plot(p_range,c_list,'ko')
zeros1 = np.linspace(0.01,0.6,1000)
plt.plot(zeros1,0.5*np.ones(len(zeros1)),'k--')
plt.xlabel(r'$p$')
plt.ylabel(r'$c=N_{+}/N$')
plt.grid()
plt.title(r'Bifurcation Diagram for q=2')
plt.legend()
"""
# Trajectories in Phase Space

Another thing we can do is to create some stream-plots in our phase space, which will help us to undersatand the stability of our fixed points.

To do that we must integrate the master equations of our dynamical system, which are the following

\begin{align}
\dfrac{\partial c}{\partial t}&=(1-p)(c_{-}\theta^{q}_{-}-c_{+}\theta^{q}_{+})+\dfrac{p}{2}(c_{-}-c_{+})\\
\dfrac{\partial b}{\partial t}&=\dfrac{2}{<k>}\sum_{j\in\{+,-\}}c_{j}\left\lbrace (1-p)\theta^{q}_{j}[<k>-2q-2(<k>-q)\theta_{j}]+p\dfrac{<k>}{2}(1-\theta_{j})\right\rbrace 
\end{align}
    
Thus the only thing we need to do is to create a function that will integrate this system for different initial conditions. And having done that we will plot $c$ versus $b$.
"""
from scipy.integrate import odeint
import pylab as pl
import sympy as sym
from scipy.optimize import fsolve


def derivative(init, t):
    '''
    define q-Voter with Independence Model
    '''
    global p, q, k
    
    # Initial conditions
    c,b = init
    
    # Derivatives
    dc = (1-p)*((1-c)*(b/(2*(1-c)))**q-c*(b/(2*c))**q)+p/2*(1-2*c)
    db = 2/k*(c*((1-p)*(b/(2*c))**q*(k-2*q-2*(k-q)*b/(2*c))+p*k/2*(1-b/(2*c)))+\
             (1-c)*((1-p)*(b/(2*(1-c)))**q*(k-2*q-2*(k-q)*b/(2*(1-c)))+p*k/2*(1-b/(2*(1-c)))))
    
    # take the output in one list
    output = np.hstack((dc,db))

    return output
"""
One way to do that is just by integrating our system and ploting our results. The problem is that our final diagram is not very helpful, so probably we should try for another method.
"""
q = 7
p = 0.111
k = 199

t_final = 2000.0
dt = 0.01
t = np.arange(0, t_final, dt)

xrange = np.linspace(0.1,0.99,5)
yrange = np.linspace(0.1,0.99,5)

for i in xrange:
    for j in yrange:
        
        x0 = [i,j]
        sol = odeint(derivative, x0, t)
        c = sol[:,0]
        b = sol[:,1]

        pl.plot(c, b,lw=4)
    
    
pl.xlabel(r"$c$",fontsize=16)
pl.ylabel(r"$b$",fontsize=16)
pl.title('Phase Space',fontsize=16)
pl.grid()
pl.show()
"""
A more enlightening way to do that is by stream-plot. Basically, it is the same idea, but it also shows the direction of the trajectories in phase space.
"""
from pylab import *

q = 7
p = 0.07
k = 20

c, b = meshgrid(arange(0.01, 0.99, 0.001), arange(0.01, 0.99, 0.001))
cdot = (1-p)*((1-c)*(b/(2*(1-c)))**q-c*(b/(2*c))**q)+p/2*(1-2*c)
bdot = 2/k*(c*((1-p)*(b/(2*c))**q*(k-2*q-2*(k-q)*b/(2*c))+p*k/2*(1-b/(2*c)))+\
             (1-c)*((1-p)*(b/(2*(1-c)))**q*(k-2*q-2*(k-q)*b/(2*(1-c)))+p*k/2*(1-b/(2*(1-c)))))

streamplot(c, b, cdot, bdot)
xlabel('c'); ylabel('b')
title('Phase Space for q={}, p={}, k={}'.format(q,p,k))
grid(); show()
q = 2
p = 0.25
k = 20

c, b = meshgrid(arange(0.01, 0.99, 0.001), arange(0.01, 0.99, 0.001))
cdot = (1-p)*((1-c)*(b/(2*(1-c)))**q-c*(b/(2*c))**q)+p/2*(1-2*c)
bdot = 2/k*(c*((1-p)*(b/(2*c))**q*(k-2*q-2*(k-q)*b/(2*c))+p*k/2*(1-b/(2*c)))+\
             (1-c)*((1-p)*(b/(2*(1-c)))**q*(k-2*q-2*(k-q)*b/(2*(1-c)))+p*k/2*(1-b/(2*(1-c)))))

streamplot(c, b, cdot, bdot)
xlabel('c'); ylabel('b')
title('Phase Space for q={}, p={}, k={}'.format(q,p,k))
grid(); show()