diff --git a/mbtrack2/tracking/__init__.py b/mbtrack2/tracking/__init__.py index 38a0f8d4fc6c3e4dceae81ae5a760f3829667432..6f72151ebe7cf8ce7a8e72a12395385fc6d08fc1 100644 --- a/mbtrack2/tracking/__init__.py +++ b/mbtrack2/tracking/__init__.py @@ -6,7 +6,11 @@ from mbtrack2.tracking.particles import (Electron, Particle) from mbtrack2.tracking.synchrotron import Synchrotron from mbtrack2.tracking.rf import (RFCavity, - CavityResonator) + CavityResonator, + ProportionalLoop, + TunerLoop, + ProportionalIntegralLoop, + DirectFeedback) from mbtrack2.tracking.parallel import Mpi from mbtrack2.tracking.element import (Element, LongitudinalMap, diff --git a/mbtrack2/tracking/monitors/monitors.py b/mbtrack2/tracking/monitors/monitors.py index dc6aecf89aa344e47f953ee201ac1101d05bd874..5d55db84cc2681d26e246f02ae3a687766559eba 100644 --- a/mbtrack2/tracking/monitors/monitors.py +++ b/mbtrack2/tracking/monitors/monitors.py @@ -1398,6 +1398,8 @@ class CavityMonitor(Monitor): group_name = cavity_name dict_buffer = {"cavity_phasor_record":(ring.h, buffer_size,), "beam_phasor_record":(ring.h, buffer_size,), + "generator_phasor_record":(ring.h, buffer_size,), + "ig_phasor_record":(ring.h, buffer_size,), "detune":(buffer_size,), "psi":(buffer_size,), "Vg":(buffer_size,), @@ -1408,6 +1410,8 @@ class CavityMonitor(Monitor): "QL":(buffer_size,)} dict_file = {"cavity_phasor_record":(ring.h, total_size,), "beam_phasor_record":(ring.h, total_size,), + "generator_phasor_record":(ring.h, total_size,), + "ig_phasor_record":(ring.h, total_size,), "detune":(total_size,), "psi":(total_size,), "Vg":(total_size,), @@ -1418,6 +1422,8 @@ class CavityMonitor(Monitor): "QL":(total_size,)} dict_dtype = {"cavity_phasor_record":complex, "beam_phasor_record":complex, + "generator_phasor_record":complex, + "ig_phasor_record":complex, "detune":float, "psi":float, "Vg":float, diff --git a/mbtrack2/tracking/monitors/plotting.py b/mbtrack2/tracking/monitors/plotting.py index 6bc498ea640a339d5944e7b3cc93251aa742b0fa..8afe5783d8cb976ddbeacf9f5fbd4e071e6e15fd 100644 --- a/mbtrack2/tracking/monitors/plotting.py +++ b/mbtrack2/tracking/monitors/plotting.py @@ -1087,15 +1087,16 @@ def plot_cavitydata(filename, cavity_name, phasor="cavity", cavity_name : str Name of the CavityResonator object. phasor : str, optional - Type of the phasor to plot. Can be "beam" or "cavity". + Type of the phasor to plot. Can be "beam", "cavity", "generator" or + "ig". plot_type : str, optional Type of plot: - "bunch" plots the phasor voltage and angle versus time for a given bunch. - "turn" plots the phasor voltage and ange versus bunch index for a given turn. - - "streak_volt" plots the phasor voltage versus bunch index and - time. + - "streak_amplitude" plots the phasor amplitude versus bunch index + and time. - "streak_angle" plots the phasor angle versus bunch index and time. - "detune" or "psi" plots the detuning or tuning angle versus time. @@ -1114,26 +1115,29 @@ def plot_cavitydata(filename, cavity_name, phasor="cavity", Figure object with the plot on it. """ - file = hp.File(filename, "r") cavity_data = file[cavity_name] time = np.array(cavity_data["time"]) - ph = {"cavity":0, "beam":1} - labels = ["Cavity", "Beam"] + ph = {"cavity":0, "beam":1, "generator":2, "ig":3} + labels = ["Cavity", "Beam", "Generator", "Generator"] + units = [" voltage [MV]", " voltage [MV]", " voltage [MV]", " current [A]"] + units_val = [1e-6, 1e-6, 1e-6, 1] if plot_type == "bunch": data = [cavity_data["cavity_phasor_record"][bunch_number,:], - cavity_data["beam_phasor_record"][bunch_number,:]] + cavity_data["beam_phasor_record"][bunch_number,:], + cavity_data["generator_phasor_record"][bunch_number,:], + cavity_data["ig_phasor_record"][bunch_number,:]] - ylabel1 = labels[ph[phasor]] + " voltage [MV]" + ylabel1 = labels[ph[phasor]] + units[ph[phasor]] ylabel2 = labels[ph[phasor]] + " phase [rad]" fig, ax = plt.subplots() twin = ax.twinx() - p1, = ax.plot(time, np.abs(data[ph[phasor]])*1e-6, color="r",label=ylabel1) + p1, = ax.plot(time, np.abs(data[ph[phasor]])*units_val[ph[phasor]], color="r",label=ylabel1) p2, = twin.plot(time, np.angle(data[ph[phasor]]), color="b", label=ylabel2) ax.set_xlabel("Turn number") ax.set_ylabel(ylabel1) @@ -1150,20 +1154,20 @@ def plot_cavitydata(filename, cavity_name, phasor="cavity", index = np.array(time) == turn if (index.size == 0): raise ValueError("Turn is not valid.") - ph = {"cavity":0, "beam":1} - data = [np.array(cavity_data["cavity_phasor_record"])[:,index], - np.array(cavity_data["beam_phasor_record"])[:,index]] - labels = ["Cavity", "Beam"] + data = [cavity_data["cavity_phasor_record"][:,index], + cavity_data["beam_phasor_record"][:,index], + cavity_data["generator_phasor_record"][:,index], + cavity_data["ig_phasor_record"][:,index]] h=len(data[0]) x=np.arange(h) - ylabel1 = labels[ph[phasor]] + " voltage [MV]" + ylabel1 = labels[ph[phasor]] + units[ph[phasor]] ylabel2 = labels[ph[phasor]] + " phase [rad]" fig, ax = plt.subplots() twin = ax.twinx() - p1, = ax.plot(x, np.abs(data[ph[phasor]])*1e-6, color="r",label=ylabel1) + p1, = ax.plot(x, np.abs(data[ph[phasor]])*units_val[ph[phasor]], color="r",label=ylabel1) p2, = twin.plot(x, np.angle(data[ph[phasor]]), color="b", label=ylabel2) ax.set_xlabel("Bunch index") ax.set_ylabel(ylabel1) @@ -1175,14 +1179,19 @@ def plot_cavitydata(filename, cavity_name, phasor="cavity", ax.yaxis.label.set_color("r") twin.yaxis.label.set_color("b") - if plot_type == "streak_volt" or plot_type == "streak_phase": + if plot_type == "streak_amplitude" or plot_type == "streak_phase": + + data = [cavity_data["cavity_phasor_record"][:,:], + cavity_data["beam_phasor_record"][:,:], + cavity_data["generator_phasor_record"][:,:], + cavity_data["ig_phasor_record"][:,:]] - if plot_type == "streak_volt": - data = np.transpose(np.abs(cavity_data["cavity_phasor_record"][:,:])*1e-6) - ylabel = labels[ph[phasor]] + " voltage [MV]" + if plot_type == "streak_amplitude": + data = np.transpose(np.abs(data[ph[phasor]])*units_val[ph[phasor]]) + ylabel = labels[ph[phasor]] + units[ph[phasor]] cmap = mpl.cm.coolwarm # diverging elif plot_type == "streak_phase": - data = np.transpose(np.angle(cavity_data["cavity_phasor_record"][:,:])) + data = np.transpose(np.angle(data[ph[phasor]])) ylabel = labels[ph[phasor]] + " phase [rad]" cmap = mpl.cm.coolwarm # diverging diff --git a/mbtrack2/tracking/rf.py b/mbtrack2/tracking/rf.py index 69a1c20f28f74503f9a11ab24ccc68a187418086..23c2de6db6e1089b61138a192fdd7a771b9f8938 100644 --- a/mbtrack2/tracking/rf.py +++ b/mbtrack2/tracking/rf.py @@ -58,6 +58,9 @@ class CavityResonator(): If used with mpi, beam.mpi.share_distributions must be called before the track method call. + Different kind of RF feeback and loops can be added using: + cavity_resonator.feedback.append(loop) + Parameters ---------- ring : Synchrotron object @@ -93,10 +96,15 @@ class CavityResonator(): Last beam phasor value of each bunch in [V]. generator_phasor : complex Generator phasor in [V]. + generator_phasor_record : array of complex + Last generator phasor value of each bunch in [V]. cavity_phasor : complex Cavity phasor in [V]. cavity_phasor_record : array of complex Last cavity phasor value of each bunch in [V]. + ig_phasor_record : array of complex + Last current generator phasor of each bunch in [A]. + Only used for some feedback types. cavity_voltage : float Cavity total voltage in [V]. cavity_phase : float @@ -182,7 +190,7 @@ class CavityResonator(): factories. In Frontiers of Particle Beams: Factories with e+ e-Rings (pp. 293-311). Springer, Berlin, Heidelberg. - [2] Yamamoto, Naoto, Alexis Gamelin, and Ryutaro Nagaoka. "Investigation + [2] Naoto Yamamoto, Alexis Gamelin, and Ryutaro Nagaoka. "Investigation of Longitudinal Beam Dynamics With Harmonic Cavities by Using the Code Mbtrack." IPAC’19, Melbourne, Australia, 2019. """ @@ -202,6 +210,7 @@ class CavityResonator(): self.theta = theta self.beam_phasor = np.zeros(1, dtype=complex) self.beam_phasor_record = np.zeros((self.ring.h), dtype=complex) + self.generator_phasor_record = np.zeros((self.ring.h), dtype=complex) self.tracking = False self.Vg = 0 self.theta_g = 0 @@ -209,6 +218,7 @@ class CavityResonator(): self.theta_gr = 0 self.Pg = 0 self.n_bin = int(n_bin) + self.feedback = [] def init_tracking(self, beam): """ @@ -295,7 +305,7 @@ class CavityResonator(): ind = (sorted_index == i) phase = self.m * self.ring.omega1 * (center0 + self.ring.T1* (index + self.ring.h * self.nturn)) - Vgene = self.Vg*np.cos(phase + self.theta_g) + Vgene = np.real(self.generator_phasor_record[index]*np.exp(1j*phase)) Vbeam = np.real(self.beam_phasor) Vtot = Vgene + Vbeam - charge_per_mp*self.loss_factor*mp_per_bin energy_change[ind] = Vtot / self.ring.E0 @@ -315,10 +325,13 @@ class CavityResonator(): # phasor decay to be at t=0 of the next bunch self.phasor_decay(self.ring.T1, ref_frame="beam") + + # apply different kind of RF feedback + for fb in self.feedback: + fb.track() self.nturn += 1 - def init_phasor_track(self, beam): """ Initialize the beam phasor for a given beam distribution using a @@ -515,7 +528,15 @@ class CavityResonator(): @property def cavity_phasor_record(self): """Last cavity phasor value of each bunch in [V]""" - return self.generator_phasor + self.beam_phasor_record + return self.generator_phasor_record + self.beam_phasor_record + + @property + def ig_phasor_record(self): + """Last current generator phasor of each bunch in [A]""" + for FB in self.feedback: + if isinstance(FB, (ProportionalIntegralLoop, DirectFeedback)): + return FB.ig_phasor_record + return np.zeros(self.ring.h) @property def cavity_voltage(self): @@ -786,6 +807,8 @@ class CavityResonator(): self.Vg = self.Vgr*np.cos(self.psi) # Generator phase [rad] self.theta_g = self.theta_gr + self.psi + # Set generator_phasor_record + self.generator_phasor_record = np.ones(self.ring.h)*self.generator_phasor def plot_phasor(self, I0): """ @@ -889,4 +912,607 @@ class CavityResonator(): def deltaVRF(self, z, I0, F = 1, PHI = 0): """Return the generator voltage minus beam loading voltage taking into account form factor amplitude F and form factor phase PHI""" - return -1*self.Vg*(self.ring.k1*self.m)**2*np.cos(self.ring.k1*self.m*z + self.theta_g) - self.Vb(I0)*F*(self.ring.k1*self.m)**2*np.cos(self.ring.k1*self.m*z + self.psi - PHI) \ No newline at end of file + return -1*self.Vg*(self.ring.k1*self.m)**2*np.cos(self.ring.k1*self.m*z + self.theta_g) - self.Vb(I0)*F*(self.ring.k1*self.m)**2*np.cos(self.ring.k1*self.m*z + self.psi - PHI) + +class ProportionalLoop(): + """ + Proportional feedback loop to control a CavityResonator amplitude and phase. + + Feedback setpoints are cav_res.Vc and cav_res.theta. + + The loop must be added to the CavityResonator object using: + cav_res.feedback.append(loop) + + Parameters + ---------- + ring : Synchrotron object + cav_res : CavityResonator + The CavityResonator which is to be controlled. + gain_A : float + Amplitude (voltage) gain of the feedback. + gain_P : float + Phase gain of the feedback. + delay : int + Feedback delay in unit of turns. + + """ + def __init__(self, ring, cav_res, gain_A, gain_P, delay): + self.ring = ring + self.cav_res = cav_res + self.gain_A = gain_A + self.gain_P = gain_P + self.delay = int(delay) + self.volt_delay = np.ones(self.delay)*self.cav_res.Vc + self.phase_delay = np.ones(self.delay)*self.cav_res.theta + + def track(self): + """ + Tracking method for the amplitude and phase loop. + + Returns + ------- + None. + + """ + diff_A = self.volt_delay[-1] - self.cav_res.Vc + diff_P = self.phase_delay[-1] - self.cav_res.theta + self.cav_res.Vg -= self.gain_A*diff_A + self.cav_res.theta_g -= self.gain_P*diff_P + self.cav_res.generator_phasor_record = np.ones(self.ring.h)*self.cav_res.generator_phasor + self.volt_delay = np.roll(self.volt_delay, 1) + self.phase_delay = np.roll(self.phase_delay, 1) + self.volt_delay[0] = self.cav_res.cavity_voltage + self.phase_delay[0] = self.cav_res.cavity_phase + +class TunerLoop(): + """ + Cavity tuner loop used to control a CavityResonator tuning (psi or detune) + in order to keep the phase between cavity and generator current constant. + + Only a proportional controller is assumed. + + The loop must be added to the CavityResonator object using: + cav_res.feedback.append(loop) + + Parameters + ---------- + ring : Synchrotron object + cav_res : CavityResonator + The CavityResonator which is to be controlled. + gain : float + Proportional gain of the tuner loop. + If not specified, 0.01 is used. + avering_period: + Period during which the phase difference is monitored and averaged. + Then the feedback correction is applied every avering_period turn. + Unit is turn number. + A value longer than one synchrotron period (1/fs) is recommended. + If not specified, 2-synchrotron period (2/fs) is used, although it is + too fast compared to the actual situation. + offset : float + Tuning offset in [rad]. + + """ + def __init__(self, ring, cav_res, gain=0.01, avering_period=0, + offset=0): + self.ring = ring + self.cav_res = cav_res + if avering_period == 0: + fs = self.ring.synchrotron_tune(self.cav_res.Vc)*self.ring.f1/self.ring.h + avering_period = 2/fs/self.ring.T0 + + self.Pgain = gain + self.offset = offset + self.avering_period = int(avering_period) + self.diff = 0 + self.count = 0 + + def track(self): + """ + Tracking method for the tuner loop. + + Returns + ------- + None. + + """ + if self.count == self.avering_period: + diff = self.diff/self.avering_period - self.offset + self.cav_res.psi -= diff * self.Pgain + self.count = 0 + self.diff = 0 + else: + self.diff += self.cav_res.cavity_phase - self.cav_res.theta_g + self.cav_res.psi + self.count += 1 + +class ProportionalIntegralLoop(): + """ + Proportional Integral (PI) loop to control a CavityResonator amplitude and + phase via generator current (ig) [1,2]. + + The ProportionalIntegralLoop should be initialized only after generator + parameters are set. + + The loop must be added to the CavityResonator object using: + cav_res.feedback.append(loop) + + Parameters + ---------- + ring : Synchrotron object + cav_res : CavityResonator object + CavityResonator in which the loop will be added. + gain : float or float list + Pgain and Igain of the feedback system + For IQ feedback, same gain set is used for I and Q. + In case of normal conducting cavity (QL~1e4), + the Pgain of ~1.0 and Igain of ~1e4(5) are usually used. + In case of super conducting cavity (QL > 1e6), + the Pgain of ~100 can be used. + In a "bad" parameter set, unstable oscillation of Vc is caused. + So, parameter scan of Vcloop should be made. + sample_num : int + Sample number to monitor Vc + The averaged Vc value in sample_num is monitored. + Units are in bucket numbers. + every : int + Interval to monitor and change Vc + Units are in bucket numbers. + delay : int + Loop delay of the PI feedback system. + Units are in bucket numbers. + Vref : array or complex, optional + Target (set point) value of the feedback. + If None, cav_res.Vc and cav_res.theta are set as the reference. + Default is None. + FF : bool, optional + True is recommended to prevent a Vc drop in the beginning of the tracking. + In case of small Pgain (QL ~ 1e4), Vc drop may cause baem loss due to static Robinson. + Default is True. + matrix : bool, optional + If True, Ig2Vg_matrix is used in Ig2Vg. + Ig2Vg_matrix is faster but init_Ig2Vg_matrix must be called after each + parameter change of cav_res. + If False, Ig2Vg_formula is used in Ig2Vg. + Default is False. + + Methods + ------- + track() + Tracking method for the Cavity PI control feedback. + Ig2Vg_formula() + Return Vg from Ig. + init_Ig2Vg_matrix() + Initialize matrix for Ig2Vg_matrix. + Ig2Vg_matrix() + Return Vg from Ig using matrix formalism. + Ig2Vg() + Go from Ig to Vg and apply values. + Vg2Ig(Vg) + Return Ig from Vg (assuming constant Vg). + + Notes + ----- + Assumption : delay >> every ~ sample_num + + Adjusting ig(Vg) parameter to keep the Vc constant (to target(Vref) values). + The "track" method is + 1) Monitoring the Vc phasor + Mean Vc value between specified bunch number (sample_num) is monitored + with specified interval period (every). + 2) Changing the ig phasor + The ig phasor is changed according the difference of the specified + reference values (Vref) with specified gain (gain). + By using ig instead of Vg, the cavity response can be taken account. + 3) ig changes are reflected to Vg after the specifed delay (delay) of the system + + Vc,IQ-->(rot)-->(-)-->(V->I,fac)-->PI-->Ig,IQ {--> Vg,IQ} + | + Ref + + Examples + -------- + PF; E0=2.5GeV, C=187m, frf=500MHz + QL=11800, fs0=23kHz + ==> gain=[0.5,1e4], sample_num=8, every=7(13ns), delay=500(1us) + is one reasonable parameter set. + + References + ---------- + [1] : https://en.wikipedia.org/wiki/PID_controller + [2] : Yamamoto, N., Takahashi, T., & Sakanaka, S. (2018). Reduction and + compensation of the transient beam loading effect in a double rf system + of synchrotron light sources. PRAB, 21(1), 012001. + + """ + def __init__(self, ring, cav_res, gain, sample_num, every, delay, + Vref=None, FF=True, matrix=False): + self.ring = ring + self.cav_res = cav_res + self.Ig2Vg_mat = np.zeros((self.ring.h, self.ring.h), dtype=complex) + self.gain = gain + self.FF = FF + if not self.FF: + self.FFconst = 0 + self.matrix = matrix + + if delay > 0: + self.delay = int(delay) + else: + self.delay = 1 + if every > 0: + self.every = int(every) + else: + self.every = 1 + record_size = int(np.ceil(self.delay/self.every)) + if record_size < 1: + raise ValueError("Bad parameter set : delay or sample_every") + self.sample_num = int(sample_num) + + # init lists for FB process + self.ig_phasor = np.ones(self.ring.h, dtype=complex)*self.Vg2Ig(self.cav_res.generator_phasor) + self.ig_phasor_record = self.ig_phasor + self.vc_previous = np.ones(self.sample_num)*self.cav_res.cavity_phasor + self.diff_record = np.zeros(record_size, dtype=complex) + self.I_record = 0+0j + + if Vref is None: + self.Vref = self.cav_res.Vc*np.exp(1j*self.cav_res.theta) + else: + self.Vref = Vref + + self.sample_list = range(0, self.ring.h, self.every) + + # Pre caclulation for Ig2Vg + if self.matrix: + self.init_Ig2Vg_matrix() + + def track(self, apply_changes=True): + """ + Tracking method for the Cavity PI control feedback. + + Returns + ------- + None. + + """ + vc_list = np.concatenate([self.vc_previous, self.cav_res.cavity_phasor_record]) #This line is slowing down the process. + self.ig_phasor.fill(self.ig_phasor[-1]) + + for index in self.sample_list: + # 2) updating Ig using last item of the list + diff = self.diff_record[-1] - self.FFconst + self.I_record += diff/self.ring.f1 + fb_value = self.gain[0]*diff + self.gain[1]*self.I_record + self.ig_phasor[index:] = self.Vg2Ig(fb_value) + self.FFconst + # Shift the record + self.diff_record = np.roll(self.diff_record, 1) + # 1) recording diff as a first item of the list + mean_vc = np.mean(vc_list[index:self.sample_num+index])*self.Rot + self.diff_record[0] = self.VrefRot - mean_vc + # update sample_list for next turn + self.sample_list = range(index+self.every-self.ring.h,self.ring.h,self.every) + # update vc_previous for next turn + self.vc_previous = self.cav_res.cavity_phasor_record[- self.sample_num:] + + self.ig_phasor_record = self.ig_phasor + + if apply_changes: + self.Ig2Vg() + + @property + def Vref(self): + """Return the target (set point) value of the feedback as a complex.""" + return self._Vref + + @Vref.setter + def Vref(self, value): + """ + Set the target (set point) value of the feedback. + + Vref can be specified as an IQ-complex style or array-like [Amp, Phase]. + If not specified, cav_res.Vc and cav_res.theta are used. + + """ + if isinstance(value,complex): + self._Vref = value + else: + self._Vref = value[0]*np.exp(1j*value[1]) + self.Rot = np.exp(-1j*np.angle(self._Vref)) + self.VrefRot = self._Vref*self.Rot + if self.FF: + self.FFconst = np.mean(self.ig_phasor) + + def Ig2Vg_formula(self): + """ + Return Vg from Ig. + + Eq.26 of ref [2]. + """ + generator_phasor_record = np.zeros_like(self.cav_res.generator_phasor_record, dtype=complex) + for index in range(self.ring.h): + generator_phasor_record[index] = (self.cav_res.generator_phasor_record[index-1] + *np.exp(-1/self.cav_res.filling_time*(1 - 1j*np.tan(self.cav_res.psi))*self.ring.T1) + + self.ig_phasor_record[index]*self.cav_res.loss_factor*self.ring.T1) + return generator_phasor_record + + def init_Ig2Vg_matrix(self): + """ + Initialize matrix for Ig2Vg_matrix. + + Shoud be called before first use of Ig2Vg_matrix and after each cavity + parameter change. + """ + k = np.arange(0, self.ring.h) + self.Ig2Vg_vec = np.exp(-1/self.cav_res.filling_time*(1 - 1j*np.tan(self.cav_res.psi))*self.ring.T1*(k+1)) + tempV = np.exp(-1/self.cav_res.filling_time*self.ring.T1*k*(1 - 1j*np.tan(self.cav_res.psi))) + for idx in np.arange(self.ring.h): + self.Ig2Vg_mat[idx:,idx] = tempV[:self.ring.h-idx] + + def Ig2Vg_matrix(self): + """ + Return Vg from Ig using matrix formalism. + Faster but self.init_Ig2Vg should be called after each CavityResonator + parameter change. + """ + generator_phasor_record = (self.Ig2Vg_vec*self.cav_res.generator_phasor_record[-1] + + self.Ig2Vg_mat.dot(self.ig_phasor_record)*self.cav_res.loss_factor*self.ring.T1) + return generator_phasor_record + + def Ig2Vg(self): + """ + Go from Ig to Vg. + + Apply new values to cav_res.generator_phasor_record, cav_res.Vg and + cav_res.theta_g from ig_phasor_record. + """ + if self.matrix: + self.cav_res.generator_phasor_record = self.Ig2Vg_matrix() + else: + self.cav_res.generator_phasor_record = self.Ig2Vg_formula() + self.cav_res.Vg = np.mean(np.abs(self.cav_res.generator_phasor_record)) + self.cav_res.theta_g = np.mean(np.angle(self.cav_res.generator_phasor_record)) + + def Vg2Ig(self, Vg): + """ + Return Ig from Vg (assuming constant Vg). + + Eq.25 of ref [2] assuming the dVg/dt = 0. + """ + fac = 1/self.cav_res.filling_time/self.cav_res.loss_factor # 1/R_L + ig = fac*Vg*( 1 - 1j*np.tan(self.cav_res.psi) ) + return ig + +class DirectFeedback(ProportionalIntegralLoop): + """ + Direct RF FB (DFB) via generator current (ig) using PI controller [1]. + + The DirectFeedback inherits from ProportionalIntegralLoop so all + mandatory parameters from ProportionalIntegralLoop should be passed as + kwargs when initializing a DirectFeedback object. + + To avoid cavity-beam unmatching (large synchrotron oscilation of beam), + the CavityResonator generator parameters should be set before + initialization. + + Parameters + ---------- + DFB_gain : float + Gain of the DFB. + DFB_phase_shift : float + Phase shift of the DFB. + DFB_sample_num : int, optional + Sample number to monitor Vc for the DFB. + The averaged Vc value in DFB_sample_num is monitored. + Units are in bucket numbers. + If None, value from the ProportionalIntegralLoop is used. + Default is None. + DFB_every : int, optional + Interval to monitor and change Vc for the DFB. + Units are in bucket numbers. + If None, value from the ProportionalIntegralLoop is used. + Default is None. + DFB_delay : int, optional + Loop delay of the DFB. + Units are in bucket numbers. + If None, value from the ProportionalIntegralLoop is used. + Default is None. + + Attributes + ---------- + phase_shift : float + Phase shift applied to DFB, defined as psi - DFB_phase_shift. + DFB_psi: float + Return detuning angle value with Direct RF feedback in [rad]. + DFB_alpha : float + Return the amplitude ratio alpha of the DFB. + DFB_gamma : float + Return the gain gamma of the DFB. + DFB_Rs : float + Return the shunt impedance of the DFB in [ohm]. + + Methods + ------- + DFB_parameter_set(DFB_gain, DFB_phase_shift) + Set DFB gain and phase shift parameters. + track() + Tracking method for the Direct RF feedback. + DFB_Vg() + Return the generator voltage main and DFB components in [V]. + DFB_fs() + Return the modified synchrotron frequency in [Hz]. + + References + ---------- + [1] : Akai, K. (2022). Stability analysis of rf accelerating mode with + feedback loops under heavy beam loading in SuperKEKB. PRAB, 25(10), + 102002. + + """ + def __init__(self, DFB_gain, DFB_phase_shift, DFB_sample_num=None, + DFB_every=None, DFB_delay=None, **kwargs): + super(DirectFeedback, self).__init__(**kwargs) + + if DFB_delay is not None: + self.DFB_delay = int(DFB_delay) + else: + self.DFB_delay = self.delay + + if DFB_sample_num is not None: + self.DFB_sample_num = int(DFB_sample_num) + else: + self.DFB_sample_num = self.sample_num + + if DFB_every is not None: + self.DFB_every = int(DFB_every) + else: + self.DFB_every = self.every + + record_size = int(np.ceil(self.DFB_delay/self.DFB_every)) + if record_size < 1: + raise ValueError("Bad parameter set : delay or sample_every") + + self.DFB_parameter_set(DFB_gain, DFB_phase_shift) + if np.sum(np.abs(self.cav_res.beam_phasor)) == 0: + cavity_phasor = self.cav_res.Vc*np.exp(1j*self.cav_res.theta) + else: + cavity_phasor = np.mean(self.cav_res.cavity_phasor_record) + self.DFB_VcRecord = np.ones(record_size, dtype=complex)*cavity_phasor + self.DFB_vc_previous = np.ones(self.DFB_sample_num, dtype=complex)*cavity_phasor + + self.DFB_sample_list = range(0, self.ring.h, self.DFB_every) + + @property + def DFB_phase_shift(self): + """Return DFB phase shift.""" + return self._DFB_phase_shift + + @DFB_phase_shift.setter + def DFB_phase_shift(self, value): + """Set DFB_phase_shift and phase_shift""" + self._DFB_phase_shift = value + self._phase_shift = self.cav_res.psi - value + + @property + def phase_shift(self): + """ + Return phase shift applied to DFB. + Defined as self.cav_res.psi - self.DFB_phase_shift. + """ + return self._phase_shift + + @property + def DFB_psi(self): + """ + Return detuning angle value with Direct RF feedback in [rad]. + + Fig.4 of ref [1]. + """ + return (np.angle(np.mean(self.cav_res.cavity_phasor_record)) - + np.angle(np.mean(self.ig_phasor_record))) + + @property + def DFB_alpha(self): + """ + Return the amplitude ratio alpha of the DFB. + + Near Eq. (13) of [1]. + """ + fac = np.abs(np.mean(self.DFB_ig_phasor)/np.mean(self.ig_phasor_record)) + return 20*np.log10(fac) + + @property + def DFB_gamma(self): + """ + Return the gain gamma of the DFB. + + Near Eq. (13) of [1]. + """ + fac = np.abs(np.mean(self.DFB_ig_phasor)/np.mean(self.ig_phasor_record)) + return fac/(1-fac) + + @property + def DFB_Rs(self): + """ + Return the shunt impedance of the DFB in [ohm]. + + Eq. (15) of [1]. + """ + return self.cav_res.Rs/(1+self.DFB_gamma*np.cos(self.DFB_psi)) + + def DFB_parameter_set(self, DFB_gain, DFB_phase_shift): + """ + Set DFB gain and phase shift parameters. + + Parameters + ---------- + DFB_gain : float + Gain of the DFB. + DFB_phase_shift : float + Phase shift of the DFB. + + """ + self.DFB_gain = DFB_gain + self.DFB_phase_shift = DFB_phase_shift + + if np.sum(np.abs(self.cav_res.beam_phasor)) == 0: + vc = np.ones(self.ring.h)*self.cav_res.Vc*np.exp(1j*self.cav_res.theta) + else: + vc = self.cav_res.cavity_phasor_record + vg_drf = self.DFB_gain*vc*np.exp(1j*self.phase_shift) + self.DFB_ig_phasor = self.Vg2Ig(vg_drf) + + self.ig_phasor = self.ig_phasor_record - self.DFB_ig_phasor + if self.FF: + self.FFconst = np.mean(self.ig_phasor) + + def track(self): + """ + Tracking method for the Direct RF feedback. + + Returns + ------- + None. + + """ + super(DirectFeedback, self).track(False) + + vc_list = np.concatenate([self.DFB_vc_previous, self.cav_res.cavity_phasor_record]) + self.DFB_ig_phasor = np.roll(self.DFB_ig_phasor, 1) + for index in self.DFB_sample_list: + # 2) updating Ig using last item of the list + vg_drf = self.DFB_gain*self.DFB_VcRecord[-1]*np.exp(1j*self.phase_shift) + self.DFB_ig_phasor[index:] = self.Vg2Ig(vg_drf) + # Shift the record + self.DFB_VcRecord = np.roll(self.DFB_VcRecord, 1) + # 1) recording Vc + mean_vc = np.mean(vc_list[index:self.DFB_sample_num+index]) + self.DFB_VcRecord[0] = mean_vc + # update sample_list for next turn + self.DFB_sample_list = range(index+self.DFB_every-self.ring.h, self.ring.h, self.DFB_every) + # update vc_previous for next turn + self.DFB_vc_previous = self.cav_res.cavity_phasor_record[- self.DFB_sample_num:] + + self.ig_phasor_record = self.ig_phasor + self.DFB_ig_phasor + + self.Ig2Vg() + + def DFB_Vg(self, vc=-1): + """Return the generator voltage main and DFB components in [V].""" + if vc == -1: + vc = np.mean(self.cav_res.cavity_phasor_record) + vg_drf=self.DFB_gain*vc*np.exp(1j*self.phase_shift) + vg_main=np.mean(self.cav_res.generator_phasor_record)-vg_drf + return vg_main, vg_drf + + def DFB_fs(self, vg_main=-1, vg_drf=-1): + """Return the modified synchrotron frequency in [Hz].""" + vc = np.mean(self.cav_res.cavity_phasor_record) + if vg_drf ==-1: + vg_drf=self.DFB_gain*vc*np.exp(1j*self.phase_shift) + if vg_main == -1: + vg_main=np.mean(self.cav_res.generator_phasor_record)-vg_drf + vg_sum = np.abs(vg_main)*np.sin(np.angle(vg_main))+np.abs(vg_drf)*np.sin(np.angle(vg_drf)) + omega_s = 0 + if (vg_sum) > 0.0: + omega_s=np.sqrt(self.ring.ac*self.ring.omega1*(vg_sum)/self.ring.E0/self.ring.T0) + return omega_s/2/np.pi