File size: 7,990 Bytes
7aab566
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import numpy as np

def normaliazed_direction(ori, des):
    dir_ = des-ori
    nor_dir_ = dir_/np.sqrt(np.sum(dir_**2))
    nor_dir_=np.nan_to_num(nor_dir_)
    return nor_dir_

def distance(ori, des):
    dis_ = des-ori
    return dis_

def acc(F, m):
    Npar=F.shape[0]
    Nvar=F.shape[1]
    # Acceleration Matrix
    Ac = np.full((Npar, Nvar), 0.0)
    for ind_r, row in enumerate(Ac):
        for ind_c, col in enumerate(row):
            Ac[ind_r][ind_c]=F[ind_r][ind_c]/m[ind_r]
    return Ac

def F_coulomb(x, v, q, P, minimize=True, k=0.0001, weight_method='Linear', t=1.0, max_q=100.0): 
    """ The procedure that generates the new positions of the particles is based on how particles with positive and negative charges move
        in electric fields. Thus, the procedure will be as follows:
        
        1. n particles are created at random positions within the search space.
        2. Each particle is assigned a positive charge type.
        3. The magnitude of each particle's charge is directly related to the objective function.
        3.1 Assignment Methods -> Linear, Exponential, Quadratic (better assignment methods can be explored)
        3.2 Simply put, a solution range is established and adjusted to a charge range between 0 to max_q according to some assignment method.
        4. Each particle holds a velocity. These start at rest but will change over the iterations of the algorithm.
        5. The P particles with the best values obtained from the iteration will remain at rest and emit an electric field of opposite sign (negative) attracting the rest.
        5.1 Let E = k*Q/r^2 be the magnetic field at a point located at a distance r from the source.
        5.2 Let Fe be the electric force between the magnetic field and the particle -> Fe = E*q0 where q0 is the particle's charge.
        5.3 Let Fij be the force produced between particle i and j -> Fij = k*qi*qj/(r^2) where r is the distance between the particles, qi and qj are the charges of i and j.
        5.4 Thus, the resultant forces of particle i are FRi = sum_{p=1}^P k*Qp*Qi/(rip^2) + sum_{j=P+1}^n k*Qi*Qj/(rij^2) 
        5.5 Moreover, by Newton's Law, F=m*a -> Fe=m*a. Thus, the velocities of each particle are calculated vf=vi+a*t -> vf = vi + (FRi/m)*t
        5.6 The positions are updated as follows x = vi*t + (a*t^2)/2 -> x = vi*t + (FRi/m)*(t^2)/2
        
        Finally, if the particles are too close, it could cause a problem. If rij^2 << 0, then the electric force would be too strong, leading
        the particles to abruptly separate from each other. To avoid this behavior, a small distance (0.00000001) is established. If the distance
        between two particles is smaller than that distance, it will be considered that both particles have collided. Then, if more than half of 
        the particles have collided, we will apply a random function to generate new solutions over a reduced search space.
    
    Args:
        x (array(size=(Npar, Nvar)): current particle positions
        v (array(size=(Npar, Nvar)): current particle velocity
        q (array(size=(Npar, 1))): current electric charge of each particle
        P (int): quantity of electric fields (the best P particles become electric fields)
        minimize (bool, optional): solver objective. Defaults to True.
        k (float, optional): electric constant. Defaults to 0.0001.
        weight_method (str, optional): method to reassign electric charges. Defaults to 'Linear'. Options=['Linear', 'Quadratic', 'Exponential'].
        t (float, optional): time. Defaults to 1.0.
        max_q (float, optional): upper bound of electric charge to assing. Defaults to 100.0.

    Returns:
        F_total, Ac, vf, new_pos: returns the force obtained on each particle, their acceleration, their velocities
                                  and their new positions
    """

    Npar=x.shape[0] 
    Nvar=x.shape[1]

    # Distance Matrix
    dis = []
    for ind_r, row in enumerate(x):
        for ind_c, col in enumerate(x):
            dis.append(distance(x[ind_r], x[ind_c]))
    dis=np.array(dis).reshape((Npar,Npar,Nvar))

    # Direction Matrix
    d = []
    for ind_r, row in enumerate(x):
        for ind_c, col in enumerate(x):
            d.append(normaliazed_direction(x[ind_r], x[ind_c]))
    d=np.array(d).reshape((Npar,Npar,Nvar))
    
    colisioned_part = []
    r_2 = np.zeros((Npar, Npar))
    for ind_r, row in enumerate(dis):
        for ind_c, col in enumerate(dis):
            value = dis[ind_r][ind_c]
            value_2 = value**2
            value_sum = np.sum(value_2)
            if value_sum < 0.00000001: # Particles have practically collided. Notice later that fe=0.0 ahead.
                r_2[ind_r][ind_c] = 0.0
                if ind_r != ind_c:
                    colisioned_part.append(ind_c)
            else:
                r_2[ind_r][ind_c] = value_sum
    colisioned_part_ = np.unique(np.array(colisioned_part))


    # Each particle is assigned an electric charge magnitude based on the solution provided by the particle.
    q=np.array(q)
    min_value = q[0]
    max_value = q[-1]
    if minimize:
        q = -1.0*(q-max_value-1.0)
        max_value = q[0]

    if weight_method=="Linear":
        # We adjust the charges according to the following range [0, max_q].
        q = (q/max_value)*max_q
        reverse_ind = [len(q)-i-1 for i in range(len(q))] # It is inverted to give more charge to the particles that are further away.
        q = q[reverse_ind]
        
    elif weight_method=="Quadratic":
        # We adjust the charges according to the following range [0, max_q].
        q_2=q**2
        q=(q_2/np.max(q_2))*max_q
        reverse_ind = [len(q)-i-1 for i in range(len(q))] # It is inverted to give more charge to the particles that are further away.
        q = q[reverse_ind]
        
    elif weight_method=="Exponential":
        # We adjust the charges according to the following range [0, max_q].
        q_exp=np.array([np.exp(i) for i in q])
        q=(q_exp/np.max(q_exp))*max_q
        reverse_ind = [len(q)-i-1 for i in range(len(q))] # It is inverted to give more charge to the particles that are further away.
        q = q[reverse_ind]
        
    Npar=d.shape[0]
    Nvar=d.shape[2]
    F=np.full((Npar, Npar, Nvar), 0.0)
    for ind_r, row in enumerate(dis):
        for ind_c, col in enumerate(row):
            # The magnitude of the electric force Fe is calculated.            
            d_2=r_2[ind_r][ind_c]
            if d_2==0:
                Fe=0.0
            else:
                q1=q[ind_r]
                q2=q[ind_c]
                Fe=float(k*q1*q2/d_2)
                if ind_r >= P and ind_c >= P: # Repulsive forces are generated between particles of the same sign.
                    F[ind_r][ind_c]=-1.0*Fe*d[ind_r][ind_c]
                else: # There is attraction between particles and electric fields.
                    F[ind_r][ind_c]=Fe*d[ind_r][ind_c]
    F[:P, :P] = 0.0
    F_total = np.sum(F, axis=1)
    F_total[:P]=0.0
    # Remember that F=ma -> m1*a1 = m2*a2. So, if m1>m2 -> a2>a1 -> Particles with greater mass have less acceleration.
    # For this method, the weight of the particle is not important, so we will set them all to be equal to 1.0.
    m=np.ones(Npar)
    # vf = acc + vi*t
    Ac=acc(F_total, m)
    vf = Ac + t*v
    Ac[:P]=0.0
    vf[:P]=0.0 # The velocity of the magnetic fields is set to 0.0.
    # Finally, Xf = xi + vi*t + 0.5*acc*(t^2) (Final Position)
    x__=v*t + 0.5*Ac*(t**2)
    new_pos = x + x__
    
    # If more than half of the particles have collided, those that have collided will move randomly within the reduced search space.
    max_crash_part=Npar/2
    if len(colisioned_part_)>max_crash_part:
        lo = np.min(new_pos, axis=0)
        up = np.max(new_pos, axis=0)
        for i in colisioned_part_:
            new_pos[i] = np.random.uniform(low=lo, high=up, size=(1, Nvar))

    return F_total, Ac, vf, new_pos