Spaces:
Runtime error
Runtime error
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 | |