In [4]:
# define the same transfer matrix as we calculate the twiss, ignore the dispersion term

import numpy as np

def D(l):
    return np.array([[1,l,0,0],
                    [0,1,0,0],
                    [0,0,1,l],
                    [0,0,0,1]])

def QF(l,k):
    sk=np.sqrt(k)
    skl=sk*l
    return np.array([[np.cos(skl),np.sin(skl)/sk,0,0],
                     [-sk*np.sin(skl),np.cos(skl),0,0],
                     [0,0,np.cosh(skl),np.sinh(skl)/sk],
                     [0,0,sk*np.sinh(skl),np.cosh(skl)]])

def QD(l,k):
    sk=np.sqrt(np.abs(k))
    skl=sk*l
    return np.array([[np.cosh(skl),np.sinh(skl)/sk,0,0],
                     [sk*np.sinh(skl),np.cosh(skl),0,0],
                     [0,0,np.cos(skl),np.sin(skl)/sk],
                     [0,0,-sk*np.sin(skl),np.cos(skl)]])

# define a function return matrix for sector magnet(recommand rho and angle form):
def SBend(r, a):
    return np.array([[np.cos(a),    r*np.sin(a),   0,0],
                     [-np.sin(a)/r, np.cos(a),     0,0],
                     [0,0,1,r*a], # vertical as dirft space
                     [0,0,0,1]])

# return element matrix by its type and parameters
def element_matrix(*args):
    if args[0] == 'D':
        l=args[1]
        return D(l)
    elif args[0] == 'Q':
        l=args[1]
        k=args[2]
        if k>0:
            return QF(l,k)
        elif k<0:
            return QD(l,k)
        else:
            return [D(l),D(l),l] # if k=0, return as drift
    elif args[0] == 'SBend':
        l=args[1]
        angle=args[2]
        rho=l/angle
        return SBend(rho,angle)
    else:
        print('Unknown element type!')
        return None

# define a function doing the do production for a list of given matrix:
def Mdot(ml):
    mt = ml[0] # temp matrix to store the results
    if len(ml) == 1: # return if there is only one matrix
        return mt
    else:
        # do the dot production one by one from the 2nd element
        for i in range(1,len(ml)):
            mt = np.dot(ml[i],mt)
    return mt
In [5]:
# the rcs r1 lattice definition
rcs_r1=[["D",5.5,0],
["Q",0.41,0.7406566],
["D",0.8,0],
["Q",0.9,-0.579364096],
["D",1.15,0],
["Q",0.41,0.664196402],
["D",3.8,0],
["SBend",2.1,0.261799388],
["D",1.2,0],
["SBend",2.1,0.261799388],
["D",1.3,0],
["Q",0.45,0.570558026],
["D",1.3,0],
["Q",0.9,-0.579364096],
["D",0.8,0],
["Q",0.62,0.608457761],
["D",0.9,0],
["SBend",2.1,0.261799388],
["D",3.5,0],
["SBend",2.1,0.261799388],
["D",0.9,0],
["Q",0.62,0.608457761],
["D",0.8,0],
["Q",0.9,-0.579364096],
["D",1.3,0],
["Q",0.45,0.570558026],
["D",1.3,0],
["SBend",2.1,0.261799388],
["D",1.2,0],
["SBend",2.1,0.261799388],
["D",3.8,0],
["Q",0.41,0.664196402],
["D",1.15,0],
["Q",0.9,-0.579364096],
["D",0.8,0],
["Q",0.41,0.7406566],
["D",5.5,0]]
In [6]:
# A quick test to compared with our twiss script about the matrix element
ml=[]
for i,e in enumerate(rcs_r1):
    ml=[element_matrix(e[0],e[1],e[2]) for e in rcs_r1]

print("Transfer matrix for r1:\n",Mdot(ml))
print("Transfer matrix for rcs(4*r1):\n",Mdot(ml*4))
Transfer matrix for r1:
 [[-0.43160468  6.65011376  0.          0.        ]
 [-0.12236143 -0.43160468  0.          0.        ]
 [ 0.          0.          0.8359187   2.53133998]
 [ 0.          0.         -0.11900413  0.8359187 ]]
Transfer matrix for rcs(4*r1):
 [[-0.21265114  7.2035042   0.          0.        ]
 [-0.13254375 -0.21265114  0.          0.        ]
 [ 0.          0.         -0.68395546  3.36460171]
 [ 0.          0.         -0.15817769 -0.68395546]]
In [7]:
# a simple track function for one particle, p is in the shape of np.array([[x],[xp],[y],[yp]])
def track_single(tm,p):
    return np.dot(tm,p)
# track a bunch of particles
def track(tm,p):
    return np.array([track_single(tm,p[i]) for i in range(len(p))])
In [8]:
# test particle at position x=1 and y=1, both xp and yp are zero
x0=np.array([1,0,1,0]).reshape(4,1)
# track for 100 times
r=[]
TM=Mdot(ml*4)
r.append(x0) # store the initial distribution first
for i in range(100):
    x0=track_single(TM,x0)
    r.append(x0)

print("Single particle trace should paint the envelop defined by the local twiss paramters")

from matplotlib import pyplot as plt

# Extract the pairs (0, 1) and (2, 3) from the arrays
pair_01 = np.array([[r[i][0][0], r[i][1][0]] for i in range(len(r))])
pair_23 = np.array([[r[i][2][0], r[i][3][0]] for i in range(len(r))])

# plot trace on x-xp and y-yp with differen colors
plt.scatter(pair_01[:, 0], pair_01[:, 1], color='r', label='x-xp')
plt.scatter(pair_23[:, 0], pair_23[:, 1], color='b', label='y-yp')

plt.xlabel('x or y')
plt.ylabel('xp or yp')
plt.legend()
plt.show()
Single particle trace should paint the envelop defined by the local twiss paramters
In [35]:
n=1000
# generate a bunch of particles in the form of [x,xp,y,yp] uniformly distributed within -1 and 1
p=np.random.rand(n,4)
p=p*2-1
p=p.reshape(n,4,1)

# Plot x-xp, y-yp, and x-y on three subplots in a single figure
fig, axs = plt.subplots(1, 3, figsize=(18, 6))

# Plot x-xp
axs[0].scatter(p[:, 0], p[:, 1], s=5, color='b', alpha=0.5)
axs[0].set_xlabel('x')
axs[0].set_ylabel('xp')
axs[0].set_title('x versus xp')
axs[0].grid(True)

# Plot y-yp
axs[1].scatter(p[:, 2], p[:, 3], s=5, color='g', alpha=0.5)
axs[1].set_xlabel('y')
axs[1].set_ylabel('yp')
axs[1].set_title('y versus yp')
axs[1].grid(True)

# Plot x-y
axs[2].scatter(p[:, 0], p[:, 2], s=5, color='r', alpha=0.5)
axs[2].set_xlabel('x')
axs[2].set_ylabel('y')
axs[2].set_title('x versus y')
axs[2].grid(True)

plt.tight_layout()
plt.show()
In [36]:
# track the whole bunch for 100 turns and store their distribution
r=[]
r.append(p) # store initial distribution
for i in range(1,1000):
    p=track(TM,p)
    r.append(p)
In [39]:
# calculate the emittance for a given distribution: e=sqrt(mean(x^2)*mean(xp^2)-mean(x*xp))
def distribution_parameters(p):
    ex=np.sqrt(np.mean((p[:,0]-np.mean(p[:,0]))**2)*np.mean((p[:,1]-np.mean(p[:,1]))**2)-np.mean(p[:,0]*p[:,1])**2)
    ey=np.sqrt(np.mean((p[:,2]-np.mean(p[:,2]))**2)*np.mean((p[:,3]-np.mean(p[:,3]))**2)-np.mean(p[:,2]*p[:,3])**2)
    # beam size given as standard deviation
    sx=np.std(p[:,0])
    sy=np.std(p[:,2])
    return ex/np.pi,ey/np.pi,sx,sy
In [25]:
# a figure for distribution plot
def distribution_plot(p,xlim=[-10,10],ylim=[-10,10],scale=1000):
    # Plot x-xp, y-yp, and x-y on three subplots in a single figure
    fig, axs = plt.subplots(1, 3, figsize=(18, 6))

    ex,ey,sx,sy=distribution_parameters(p)

    # Plot x-xp
    axs[0].set_xlim(xlim)
    axs[0].set_ylim(ylim)
    axs[0].scatter(p[:, 0]*scale, p[:, 1]*scale, s=5, color='b', alpha=0.5)
    axs[0].set_xlabel('x')
    axs[0].set_ylabel('xp')
    axs[0].set_title('x-xp: {:.3f} pi.mm.mrad'.format(ex*scale*scale))
    axs[0].grid(True)

    # Plot y-yp
    axs[1].set_xlim(xlim)
    axs[1].set_ylim(ylim)
    axs[1].scatter(p[:, 2]*scale, p[:, 3]*scale, s=5, color='g', alpha=0.5)
    axs[1].set_xlabel('y')
    axs[1].set_ylabel('yp')
    axs[1].set_title('y-yp: {:.3f} pi.mm.mrad'.format(ey*scale*scale))
    axs[1].grid(True)

    # Plot x-y
    axs[1].set_xlim(xlim)
    axs[1].set_ylim(ylim)
    axs[2].scatter(p[:, 0]*scale, p[:, 2]*scale, s=5, color='r', alpha=0.5)
    axs[2].set_xlabel('x')
    axs[2].set_ylabel('y')
    axs[2].set_title('x versus y')
    axs[2].grid(True)

    plt.tight_layout()
    plt.show()
In [33]:
distribution_plot(r[99])
In [98]:
n=10000
# generate a bunch of particles in the form of [x,xp,y,yp] with gaussian distribution and sigma =1
p=np.random.randn(n,4)
# p=p.reshape(n,4,1)

# distribution_plot(p,[-10,10],[-10,10])

# track the whole bunch for 100 turns and store their distribution
rn=[]
pn=[]
rn.append(p) # store initial distribution
ex0,ey0,sx0,sy0=distribution_parameters(p)
pn.append([0,ex0,ey0,sx0,sy0])
for i in range(1,1000):
    p=track(TM,p)
    rn.append(p)
    ex,ey,sx,sy=distribution_parameters(p)
    pn.append([i+1,ex,ey,sx,sy])
In [99]:
# plot ex,ey,sx and sy in terms of i for pn

# Extract the data for plotting
turns = [entry[0] for entry in pn]
parameters = {
    'Horizontal Emittance (ex)': [entry[1] for entry in pn],
    'Vertical Emittance (ey)': [entry[2] for entry in pn],
    'Horizontal Beam Size (sx)': [entry[3] for entry in pn],
    'Vertical Beam Size (sy)': [entry[4] for entry in pn]
}

# Create a figure and axis
fig, ax = plt.subplots(figsize=(12, 8))

# Plot each parameter
for param_name, param_values in parameters.items():
    linestyle = '--' if param_name in ['Horizontal Emittance (ex)', 'Vertical Emittance (ey)'] else '-'
    ax.plot(turns, param_values, label=param_name, linestyle=linestyle)

# Set labels and title
ax.set_xlabel('Turn Number (i)')
ax.set_ylabel('Parameter Value')
ax.set_title('Evolution of Emittance and Beam Size')

# Display legend and grid
ax.legend()
ax.grid(True)

# Show the plot
plt.show()
In [100]:
import matplotlib.animation as animation

fig, axs = plt.subplots(1, 3, figsize=(15, 5))

frames=rn
n_frames=len(frames)
xlim=[-30,30]
ylim=[-30,30]
# Initialization function: plot the first frame
def init():
    axs[0].set_xlim(xlim)
    axs[0].set_ylim(ylim)
    axs[0].scatter(frames[0][:, 0], frames[0][:, 1], s=5, color='b', alpha=0.5)
    axs[0].set_xlabel('x')
    axs[0].set_ylabel('xp')
    axs[0].set_title('x versus xp')
    
    axs[1].set_xlim(xlim)
    axs[1].set_ylim(ylim)
    axs[1].scatter(frames[0][:, 2], frames[0][:, 3], s=5, color='g', alpha=0.5)
    axs[1].set_xlabel('y')
    axs[1].set_ylabel('yp')
    axs[1].set_title('y versus yp')

    axs[2].set_xlim(xlim)
    axs[2].set_ylim(ylim)
    axs[2].scatter(frames[0][:, 0], frames[0][:, 2], s=5, color='r', alpha=0.5)
    axs[2].set_xlabel('x')
    axs[2].set_ylabel('y')
    axs[2].set_title('x versus y')

# Update function: update the scatter plot for each frame
def update(frame):

    ex,ey,sx,sy=distribution_parameters(frames[frame])

    axs[0].clear()
    axs[0].set_xlim(xlim)
    axs[0].set_ylim(ylim)
    axs[0].scatter(frames[frame][:, 0], frames[frame][:, 1], s=5, color='b', alpha=0.5)
    axs[0].set_xlabel('x')
    axs[0].set_ylabel('xp')
    axs[0].set_title('x-xp: {:.3f} pi.mm.mrad'.format(ex))

    axs[1].clear()
    axs[1].set_xlim(xlim)
    axs[1].set_ylim(ylim)
    axs[1].scatter(frames[frame][:, 2], frames[frame][:, 3], s=5, color='g', alpha=0.5)
    axs[1].set_xlabel('y')
    axs[1].set_ylabel('yp')
    axs[1].set_title('y-yp: {:.3f} pi.mm.mrad'.format(ey))

    axs[2].clear()
    axs[2].set_xlim(xlim)
    axs[2].set_ylim(ylim)
    axs[2].scatter(frames[frame][:, 0], frames[frame][:, 2], s=5, color='r', alpha=0.5)
    axs[2].set_xlabel('x')
    axs[2].set_ylabel('y')
    axs[2].set_title('x - y')

    fig.suptitle('TURN {}'.format(frame), fontsize=16)  # Set the overall title for the animation

ani = animation.FuncAnimation(fig, update, frames=n_frames, init_func=init, blit=False)

ani.save('particle_distribution_n.gif', writer='pillow', fps=10)
plt.show()
In [74]:
distribution_plot(rn[0],[-30,30],[-30,30])
In [44]:
# matched beam generation
print("Notice that at the staring point, alpha_x,y is zero, \n which means that the distribution is a typical ellipse.ex=sqrt(sx^2*sxp^2-sxxp^2)")
ex0=ey0=1.0e-6 # pi.mm.mrad
bx0=7.372117913456016
by0=4.612052201102539
sx0=np.sqrt(bx0*ex0*np.pi)
sxp0=np.sqrt(ex0*np.pi/bx0)
# sxp0=ex0/sx0
sy0=np.sqrt(by0*ey0*np.pi)
syp0=np.sqrt(ey0*np.pi/by0)
# syp0=ey0/sy0

print("sx0,sxp0,sy0,syp0",[sx0*1000,sxp0*1000,sy0*1000,syp0*1000])

num_particles =10000
# Covariance matrix
cov_x = [[sx0**2, 0],
              [0, sxp0**2]]
cov_y = [[sy0**2, 0],
              [0, syp0**2]]

# Generate random samples from a multivariate normal distribution
mean = [0, 0]
x, xp = np.random.multivariate_normal(mean, cov_x, num_particles ).T
y, yp = np.random.multivariate_normal(mean, cov_y, num_particles ).T

pm = np.column_stack((x, xp, y, yp))

distribution_plot(pm,[-40,40],[-6,6])
Notice that at the staring point, alpha_x,y is zero, 
 which means that the distribution is a typical ellipse.ex=sqrt(sx^2*sxp^2-sxxp^2)
sx0,sxp0,sy0,syp0 [4.812503660082882, 0.6527979769963828, 3.806466775496191, 0.8253303756159204]
In [47]:
# track the whole bunch for 100 turns and store their distribution
rm=[]
rm.append(pm) # store initial distribution
pnm=[]
ex0,ey0,sx0,sy0=distribution_parameters(pm)
pnm.append([0,ex0,ey0,sx0,sy0])
for i in range(1,1000):
    pm=track(TM,pm)
    rm.append(pm)
    ex,ey,sx,sy=distribution_parameters(pm)
    pnm.append([i+1,ex,ey,sx,sy])
In [51]:
# plot ex,ey,sx and sy in terms of i for pn

# Extract the data for plotting
turns = [entry[0] for entry in pnm]
parameters = {
    'Horizontal Emittance (ex)': [entry[1]*1000*1000 for entry in pnm],
    'Vertical Emittance (ey)': [entry[2]*1000*1000 for entry in pnm],
    'Horizontal Beam Size (sx)': [entry[3]*1000 for entry in pnm],
    'Vertical Beam Size (sy)': [entry[4]*1000 for entry in pnm]
}

# Create a figure and axis
fig, ax = plt.subplots(figsize=(12, 8))

# Plot each parameter
for param_name, param_values in parameters.items():
    linestyle = '--' if param_name in ['Horizontal Emittance (ex)', 'Vertical Emittance (ey)'] else '-'
    ax.plot(turns, param_values, label=param_name, linestyle=linestyle)

# Set labels and title
ax.set_xlabel('Turn Number (i)')
ax.set_ylabel('Parameter Value')
ax.set_title('Evolution of Emittance and Beam Size')

# Display legend and grid
ax.legend()
ax.grid(True)

# Show the plot
plt.show()
In [53]:
import matplotlib.pyplot as plt
import matplotlib.animation as animation

def distribution_animation(rn, file_output):
    fig, axs = plt.subplots(1, 3, figsize=(15, 5))

    n_frames = len(rn)
    xlim = [-30, 30]
    ylim = [-30, 30]

    def init():
        for ax in axs:
            ax.set_xlim(xlim)
            ax.set_ylim(ylim)

    def update(frame):
        for ax in axs:
            ax.clear()
            ax.set_xlim(xlim)
            ax.set_ylim(ylim)

        ex, ey, sx, sy = distribution_parameters(rn[frame])

        axs[0].scatter(rn[frame][:, 0]*1e3, rn[frame][:, 1]*1e3, s=5, color='b', alpha=0.5)
        axs[0].set_xlabel('x[mm]')
        axs[0].set_ylabel('xp[mrad]')
        axs[0].set_title('x-xp: {:.3f} pi.mm.mrad'.format(ex*1e6))

        axs[1].scatter(rn[frame][:, 2]*1e3, rn[frame][:, 3]*1e3, s=5, color='g', alpha=0.5)
        axs[1].set_xlabel('y[mm]')
        axs[1].set_ylabel('yp[mrad]')
        axs[1].set_title('y-yp: {:.3f} pi.mm.mrad'.format(ey*1e6))

        axs[2].scatter(rn[frame][:, 0]*1e3, rn[frame][:, 2]*1e3, s=5, color='r', alpha=0.5)
        axs[2].set_xlabel('x[mm]')
        axs[2].set_ylabel('y[mm]')
        axs[2].set_title('x - y')

        fig.suptitle('TURN {}'.format(frame), fontsize=16)

    ani = animation.FuncAnimation(fig, update, frames=n_frames, init_func=init, blit=False)
    ani.save(file_output, writer='pillow', fps=10)
    plt.show()

# Example usage
# Assuming you have defined the distribution_parameters function
# and have rn and file_output defined
# generate_particle_distribution_animation(rn, 'particle_distribution_n.gif')
In [54]:
distribution_animation(rm,"matched_run.gif")