Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
rf.py 25.67 KiB
# -*- coding: utf-8 -*-
"""
This module handles radio-frequency (RF) cavitiy elements. 

@author: gamelina
@date: 11/03/2020
"""

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.legend_handler import HandlerPatch
from mbtrack2.tracking.element import Element

class RFCavity(Element):
    """
    Perfect RF cavity class for main and harmonic RF cavities.
    Use cosine definition.
    
    Parameters
    ----------
    ring : Synchrotron object
    m : int
        Harmonic number of the cavity
    Vc : float
        Amplitude of cavity voltage [V]
    theta : float
        Phase of Cavity voltage
    """
    def __init__(self, ring, m, Vc, theta):
        self.ring = ring
        self.m = m 
        self.Vc = Vc
        self.theta = theta
        
    @Element.parallel    
    def track(self,bunch):
        """
        Tracking method for the element.
        No bunch to bunch interaction, so written for Bunch objects and
        @Element.parallel is used to handle Beam objects.
        
        Parameters
        ----------
        bunch : Bunch or Beam object
        """
        bunch["delta"] += self.Vc / self.ring.E0 * np.cos(
                self.m * self.ring.omega1 * bunch["tau"] + self.theta )
        
    def value(self, val):
        return self.Vc / self.ring.E0 * np.cos( 
                self.m * self.ring.omega1 * val + self.theta )
    
    
class CavityResonator():
    """Cavity resonator class for active or passive RF cavity with beam
    loading or HOM, based on [1].
    
    Parameters
    ----------
    ring : Synchrotron object
    m : int or float
        Harmonic number of the cavity.
    Rs : float
        Shunt impedance of the cavity in [Ohm], defined as 0.5*Vc*Vc/Pc.
    Q : float
        Quality factor of the cavity.
    QL : float
        Loaded quality factor of the cavity.
    detune : float
        Detuing of the cavity in [Hz], defined as (fr - m*ring.f1).
    Vc : float, optinal
        Total cavity voltage in [V].
    theta : float, optional
        Total cavity phase in [rad].
        
    Attributes
    ----------
    beam_phasor : complex
        Beam phasor in [V].
    beam_phasor_record : array of complex
        Last beam phasor value of each bunch in [V].
    generator_phasor : complex
        Generator phasor in [V].
    cavity_phasor : complex
        Cavity phasor in [V].
    cavity_phasor_record : array of complex
        Last cavity phasor value of each bunch in [V].
    cavity_voltage : float
        Cavity total voltage in [V].
    cavity_phase : float
        Cavity total phase in [rad].
    loss_factor : float
        Cavity loss factor in [V/C].
    beta : float
        Coupling coefficient of the cavity.
    fr : float
        Resonance frequency of the cavity in [Hz].
    wr : float
        Angular resonance frequency in [Hz.rad].
    psi : float
        Tuning angle in [rad].
    filling_time : float
        Cavity filling time in [s].
    Pg : float
        Generator power in [W].
    Vgr : float
        Generator voltage at resonance in [V].
    theta_gr : float
        Generator phase at resonance in [rad].
    Vg : float
        Generator voltage in [V].
    theta_g : float
        Generator phase in [rad].
    tracking : bool
        True if the tracking has been initialized.
    bunch_index : int
        Number of the tracked bunch in the current core.
    distance : array
        Distance between bunches.
    valid_bunch_index : array
    
    Methods
    -------
    Vbr(I0)
        Return beam voltage at resonance in [V].
    Vb(I0)
        Return beam voltage in [V].
    Z(f)
        Cavity impedance in [Ohm] for a given frequency f in [Hz].
    set_optimal_detune(I0)
        Set detuning to optimal conditions.
    set_generator(I0)
        Set generator parameters.
    plot_phasor(I0)
        Plot phasor diagram.
    is_DC_Robinson_stable(I0)
        Check DC Robinson stability.
    plot_DC_Robinson_stability()
        Plot DC Robinson stability limit.
    init_tracking(beam)
        Initialization of the tracking.
    track(beam)
        Tracking method.
    phasor_decay(time)
        Compute the beam phasor decay during a given time span.
    phasor_evol(profile, bin_length, charge_per_mp)
        Compute the beam phasor evolution during the crossing of a bunch.
    VRF(z, I0)
        Return the total RF voltage.
    dVRF(z, I0)
        Return derivative of total RF voltage.
    ddVRF(z, I0)
        Return the second derivative of total RF voltage.
    deltaVRF(z, I0)
        Return the generator voltage minus beam loading voltage.
    
    References
    ----------
    [1] Wilson, P. B. (1994). Fundamental-mode rf design in e+ e− storage ring 
    factories. In Frontiers of Particle Beams: Factories with e+ e-Rings 
    (pp. 293-311). Springer, Berlin, Heidelberg.
    """
    def __init__(self, ring, m, Rs, Q, QL, detune, Vc=0, theta=0):
        self.ring = ring
        self.m = m
        self.Rs = Rs
        self.Q = Q
        self.QL = QL
        self.detune = detune
        self.Vc = Vc
        self.theta = theta
        self.beam_phasor = np.zeros(1, dtype=np.complex)
        self.beam_phasor_record = np.zeros((self.ring.h), dtype=np.complex)
        self.tracking = False
        
    def init_tracking(self, beam):
        """
        Initialization of the tracking.

        Parameters
        ----------
        beam : Beam object

        """
        if beam.mpi_switch:
            self.bunch_index = beam.mpi.bunch_num # Number of the tracked bunch in this processor
            
        self.distance = beam.distance_between_bunches
        self.valid_bunch_index = beam.bunch_index
        self.tracking = True
        self.nturn = 0
    
    def track(self, beam):
        """
        Track a Beam object through the CavityResonator object.
        
        Can be used with or without mpi.
        If used with mpi, beam.mpi_share_distributions must be called before.
        
        The beam phasor is given at t=0 (synchronous particle) of the first 
        non empty bunch.

        Parameters
        ----------
        beam : Beam object

        """
        
        if self.tracking is False:
            self.init_tracking(beam)
        
        for index, bunch in enumerate(beam):
            
            if beam.filling_pattern[index]:
                
                if beam.mpi_switch:
                    # get rank of bunch n° index
                    rank = beam.mpi.bunch_to_rank(index)
                    # mpi -> get shared bunch profile for current bunch
                    center = beam.mpi.tau_center[rank]
                    profile = beam.mpi.tau_profile[rank]
                    bin_length = center[1]-center[0]
                    charge_per_mp = beam.mpi.charge_per_mp_all[rank]
                    if index == self.bunch_index:
                        sorted_index = beam.mpi.tau_sorted_index
                else:
                    # no mpi -> get bunch profile for current bunch
                    (bins, sorted_index, profile, center) = bunch.binning()
                    bin_length = center[1]-center[0]
                    charge_per_mp = bunch.charge_per_mp
                    self.bunch_index = index
                
                energy_change = bunch["tau"]*0
                
                # remove part of beam phasor decay to be at the start of the binning (=bins[0])
                self.phasor_decay(center[0] - bin_length/2, ref_frame="beam")
                
                if index != self.bunch_index:
                    self.phasor_evol(profile, bin_length, charge_per_mp, ref_frame="beam")
                else:
                    # modify beam phasor
                    for i, center0 in enumerate(center):
                        mp_per_bin = profile[i]
                        
                        if mp_per_bin == 0:
                            self.phasor_decay(bin_length, ref_frame="beam")
                            continue
                        
                        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)
                        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
    
                        self.beam_phasor -= 2*charge_per_mp*self.loss_factor*mp_per_bin
                        self.phasor_decay(bin_length, ref_frame="beam")
                
                # phasor decay to be at t=0 of the current bunch (=-1*bins[-1])
                self.phasor_decay(-1 * (center[-1] + bin_length/2), ref_frame="beam")
                
                if index == self.bunch_index:
                    # apply kick
                    bunch["delta"] += energy_change
            
            # save beam phasor value
            self.beam_phasor_record[index] = self.beam_phasor
            
            # phasor decay to be at t=0 of the next bunch
            self.phasor_decay(self.ring.T1, ref_frame="beam")
                
        self.nturn += 1
                
        
    def init_phasor_track(self, beam):
        """
        Initialize the beam phasor for a given beam distribution using a
        tracking like method.
        
        Follow the same steps as the track method but in the "rf" reference 
        frame and without any modifications on the beam.

        Parameters
        ----------
        beam : Beam object

        """        
        if self.tracking is False:
            self.init_tracking(beam)
            
        n_turn = int(self.filling_time/self.ring.T0*10)
        
        for i in range(n_turn):
            for j, bunch in enumerate(beam.not_empty):
                
                index = self.valid_bunch_index[j]
                
                if beam.mpi_switch:
                    # get shared bunch profile for current bunch
                    center = beam.mpi.tau_center[j]
                    profile = beam.mpi.tau_profile[j]
                    bin_length = center[1]-center[0]
                    charge_per_mp = beam.mpi.charge_per_mp_all[j]
                else:
                    if i == 0:
                        # get bunch profile for current bunch
                        (bins, sorted_index, profile, center) = bunch.binning()
                        if j == 0:
                            self.profile_save = np.zeros((len(beam),len(profile),))
                            self.center_save = np.zeros((len(beam),len(center),))
                        self.profile_save[j,:] = profile
                        self.center_save[j,:] = center
                    else:
                        profile = self.profile_save[j,:]
                        center = self.center_save[j,:]
                        
                    bin_length = center[1]-center[0]
                    charge_per_mp = bunch.charge_per_mp
                
                self.phasor_decay(center[0] - bin_length/2, ref_frame="rf")
                self.phasor_evol(profile, bin_length, charge_per_mp, ref_frame="rf")
                self.phasor_decay(-1 * (center[-1] + bin_length/2), ref_frame="rf")
                self.phasor_decay( (self.distance[index] * self.ring.T1), ref_frame="rf")
            
    def phasor_decay(self, time, ref_frame="beam"):
        """
        Compute the beam phasor decay during a given time span, assuming that 
        no particles are crossing the cavity during the time span.

        Parameters
        ----------
        time : float
            Time span in [s], can be positive or negative.
        ref_frame : string, optional
            Reference frame to be used, can be "beam" or "rf".

        """
        if ref_frame == "beam":
            delta = self.wr
        elif ref_frame == "rf":
            delta = (self.wr - self.m*self.ring.omega1)
        self.beam_phasor = self.beam_phasor * np.exp((-1/self.filling_time +
                                  1j*delta)*time)
        
    def phasor_evol(self, profile, bin_length, charge_per_mp, ref_frame="beam"):
        """
        Compute the beam phasor evolution during the crossing of a bunch using 
        an analytic formula [1].
        
        Assume that the phasor decay happens before the beam loading.

        Parameters
        ----------
        profile : array
            Longitudinal profile of the bunch in [number of macro-particle].
        bin_length : float
            Length of a bin in [s].
        charge_per_mp : float
            Charge per macro-particle in [C].
        ref_frame : string, optional
            Reference frame to be used, can be "beam" or "rf".
            
        References
        ----------
        [1] mbtrack2 manual.
            
        """
        if ref_frame == "beam":
            delta = self.wr
        elif ref_frame == "rf":
            delta = (self.wr - self.m*self.ring.omega1)
            
        n_bin = len(profile)
        
        # Phasor decay during crossing time
        deltaT = n_bin*bin_length
        self.phasor_decay(deltaT, ref_frame)
        
        # Phasor evolution due to induced voltage by marco-particles
        k = np.arange(0, n_bin)
        var = np.exp( (-1/self.filling_time + 1j*delta) * 
                      (n_bin-k) * bin_length )
        sum_tot = np.sum(profile * var)
        sum_val = -2 * sum_tot * charge_per_mp * self.loss_factor
        self.beam_phasor += sum_val
        
    def init_phasor(self, beam, n_bin=75):
        """
        Initialize the beam phasor for a given beam distribution using an
        analytic formula [1].
        
        No modifications on the Beam object.

        Parameters
        ----------
        beam : Beam object
        n_bin : int, optional
            Number of bins to use. The default is 75.
            
        References
        ----------
        [1] mbtrack2 manual.

        """
        
        # Initialization
        if self.tracking is False:
            self.init_tracking(beam)
        
        N = n_bin-1
        delta = (self.wr - self.m*self.ring.omega1)
        n_turn = int(self.filling_time/self.ring.T0*10)
        
        T = np.ones(self.ring.h)*self.ring.T1
        bin_length = np.zeros(self.ring.h)
        charge_per_mp = np.zeros(self.ring.h)
        profile = np.zeros((N, self.ring.h))
        center = np.zeros((N, self.ring.h))
        
        # Gather beam distribution data
        for j, bunch in enumerate(beam.not_empty):
            index = self.valid_bunch_index[j]
            if beam.mpi_switch:
                beam.mpi_share_distributions(n_bin=n_bin)
                center[:,index] = beam.mpi.tau_center[j]
                profile[:,index] = beam.mpi.tau_profile[j]
                bin_length[index] = center[1, index]-center[0, index]
                charge_per_mp[index] = beam.mpi.charge_per_mp_all[j]
            else:
                (bins, sorted_index, profile[:, index], center[:, index]) = bunch.binning(n_bin=n_bin)
                bin_length[index] = center[1, index]-center[0, index]
                charge_per_mp[index] = bunch.charge_per_mp
            T[index] -= (center[-1, index] + bin_length[index]/2)
            if index != 0:
                T[index - 1] += (center[0, index] - bin_length[index]/2)
        T[self.ring.h - 1] += (center[0, 0] - bin_length[0]/2)

        # Compute matrix coefficients
        k = np.arange(0, N)
        Tkj = np.zeros((N, self.ring.h))
        for j in range(self.ring.h):
            sum_t = np.array([T[n] + N*bin_length[n] for n in range(j+1,self.ring.h)])
            Tkj[:,j] = (N-k)*bin_length[j] + T[j] + np.sum(sum_t)
            
        var = np.exp( (-1/self.filling_time + 1j*delta) * Tkj )
        sum_tot = np.sum((profile*charge_per_mp) * var)
        
        # Use the formula n_turn times
        for i in range(n_turn):
            # Phasor decay during one turn
            self.phasor_decay(self.ring.T0, ref_frame="rf")
            # Phasor evolution due to induced voltage by marco-particles during one turn
            sum_val = -2 * sum_tot * self.loss_factor
            self.beam_phasor += sum_val
        
        # Replace phasor at t=0 (synchronous particle) of the first non empty bunch.
        idx0 = self.valid_bunch_index[0]
        self.phasor_decay(center[-1,idx0] + bin_length[idx0]/2, ref_frame="rf")
    
    @property
    def generator_phasor(self):
        """Generator phasor in [V]"""
        return self.Vg*np.exp(1j*self.theta_g)
    
    @property
    def cavity_phasor(self):
        """Cavity total phasor in [V]"""
        return self.generator_phasor + self.beam_phasor
    
    @property
    def cavity_phasor_record(self):
        """Last cavity phasor value of each bunch in [V]"""
        return self.generator_phasor + self.beam_phasor_record
    
    @property
    def cavity_voltage(self):
        """Cavity total voltage in [V]"""
        return np.abs(self.cavity_phasor)
    
    @property
    def cavity_phase(self):
        """Cavity total phase in [rad]"""
        return np.angle(self.cavity_phasor)
    
    @property
    def beam_voltage(self):
        """Beam loading voltage in [V]"""
        return np.abs(self.beam_phasor)
    
    @property
    def beam_phase(self):
        """Beam loading phase in [rad]"""
        return np.angle(self.beam_phasor)
    
    @property
    def loss_factor(self):
        """Cavity loss factor in [V/C]"""
        return self.wr*self.Rs/(2 * self.Q)

    @property
    def m(self):
        """Harmonic number of the cavity"""
        return self._m

    @m.setter
    def m(self, value):
        self._m = value

    @property
    def Rs(self):
        """Shunt impedance [ohm]"""
        return self._Rs

    @Rs.setter
    def Rs(self, value):
        self._Rs = value

    @property
    def Q(self):
        """Quality factor"""
        return self._Q

    @Q.setter
    def Q(self, value):
        self._Q = value

    @property
    def QL(self):
        """Loaded quality factor"""
        return self._QL

    @QL.setter
    def QL(self, value):
        self._QL = value
        self._beta = self.Q/self.QL - 1

    @property
    def beta(self):
        """Coupling coefficient"""
        return self._beta

    @beta.setter
    def beta(self, value):
        self.QL = self.Q/(1 + value)

    @property
    def detune(self):
        """Cavity detuning [Hz] - defined as (fr - m*f1)"""
        return self._detune

    @detune.setter
    def detune(self, value):
        self._detune = value
        self._fr = self.detune + self.m*self.ring.f1
        self._wr = self.fr*2*np.pi
        self._psi = np.arctan(self.QL*(self.fr/(self.m*self.ring.f1) -
                                       (self.m*self.ring.f1)/self.fr))

    @property
    def fr(self):
        """Resonance frequency of the cavity in [Hz]"""
        return self._fr

    @fr.setter
    def fr(self, value):
        self.detune = value - self.m*self.ring.f1

    @property
    def wr(self):
        """Angular resonance frequency in [Hz.rad]"""
        return self._wr

    @wr.setter
    def wr(self, value):
        self.detune = (value - self.m*self.ring.f1)*2*np.pi

    @property
    def psi(self):
        """Tuning angle in [rad]"""
        return self._psi

    @psi.setter
    def psi(self, value):
        delta = (self.ring.f1*self.m*np.tan(value)/self.QL)**2 + 4*(self.ring.f1*self.m)**2
        fr = (self.ring.f1*self.m*np.tan(value)/self.QL + np.sqrt(delta))/2
        self.detune = fr - self.m*self.ring.f1
        
    @property
    def filling_time(self):
        """Cavity filling time in [s]"""
        return 2*self.QL/self.wr
        
    def Vbr(self, I0):
        """
        Return beam voltage at resonance in [V].

        Parameters
        ----------
        I0 : float
            Beam current in [A].

        Returns
        -------
        float
            Beam voltage at resonance in [V].

        """
        return 2*I0*self.Rs/(1+self.beta)
    
    def Vb(self, I0):
        """
        Return beam voltage in [V].

        Parameters
        ----------
        I0 : float
            Beam current in [A].

        Returns
        -------
        float
            Beam voltage in [V].

        """
        return self.Vbr(I0)*np.cos(self.psi)
    
    def Z(self, f):
        """Cavity impedance in [Ohm] for a given frequency f in [Hz]"""
        return self.Rs/(1 + 1j*self.QL*(self.fr/f - f/self.fr))
    
    def set_optimal_detune(self, I0):
        """
        Set detuning to optimal conditions - second Eq. (4.2.1) in [1].

        Parameters
        ----------
        I0 : float
            Beam current in [A].

        """
        self.psi = np.arctan(-self.Vbr(I0)/self.Vc*np.sin(self.theta))
                
    def set_generator(self, I0):
        """
        Set generator parameters (Pg, Vgr, theta_gr, Vg and theta_g) for a 
        given current and set of parameters.

        Parameters
        ----------
        I0 : float
            Beam current in [A].

        """
        
        # Generator power [W] - Eq. (4.1.2) [1] corrected with factor (1+beta)**2 instead of (1+beta**2)
        self.Pg = self.Vc**2*(1+self.beta)**2/(2*self.Rs*4*self.beta*np.cos(self.psi)**2)*(
            (np.cos(self.theta) + 2*I0*self.Rs/(self.Vc*(1+self.beta))*np.cos(self.psi)**2 )**2 + 
            (np.sin(self.theta) + 2*I0*self.Rs/(self.Vc*(1+self.beta))*np.cos(self.psi)*np.sin(self.psi) )**2)
        # Generator voltage at resonance [V] - Eq. (3.2.2) [1]
        self.Vgr = 2*self.beta**(1/2)/(1+self.beta)*(2*self.Rs*self.Pg)**(1/2)
        # Generator phase at resonance [rad] - from Eq. (4.1.1)
        self.theta_gr = np.arctan((self.Vc*np.sin(self.theta) + self.Vbr(I0)*np.cos(self.psi)*np.sin(self.psi))/
                    (self.Vc*np.cos(self.theta) + self.Vbr(I0)*np.cos(self.psi)**2)) - self.psi
        # Generator voltage [V]
        self.Vg = self.Vgr*np.cos(self.psi)
        # Generator phase [rad]
        self.theta_g = self.theta_gr + self.psi
        
    def plot_phasor(self, I0):
        """
        Plot phasor diagram showing the vector addition of generator and beam 
        loading voltage.

        Parameters
        ----------
        I0 : float
            Beam current in [A].
            
        Returns
        -------
        Figure.

        """

        def make_legend_arrow(legend, orig_handle,
                              xdescent, ydescent,
                              width, height, fontsize):
            p = mpatches.FancyArrow(0, 0.5*height, width, 0, length_includes_head=True, head_width=0.75*height )
            return p

        fig = plt.figure()
        ax= fig.add_subplot(111, polar=True)
        ax.set_rmax(max([1.2,self.Vb(I0)/self.Vc*1.2,self.Vg/self.Vc*1.2]))
        arr1 = ax.arrow(self.theta, 0, 0, 1, alpha = 0.5, width = 0.015,
                         edgecolor = 'black', lw = 2)

        arr2 = ax.arrow(self.psi + np.pi, 0, 0,self.Vb(I0)/self.Vc, alpha = 0.5, width = 0.015,
                         edgecolor = 'red', lw = 2)

        arr3 = ax.arrow(self.theta_g, 0, 0,self.Vg/self.Vc, alpha = 0.5, width = 0.015,
                         edgecolor = 'blue', lw = 2)

        ax.set_rticks([])  # less radial ticks
        plt.legend([arr1,arr2,arr3], ['Vc','Vb','Vg'],handler_map={mpatches.FancyArrow : HandlerPatch(patch_func=make_legend_arrow),})
        
        return fig
        
    def is_DC_Robinson_stable(self, I0):
        """
        Check DC Robinson stability - Eq. (6.1.1) [1]

        Parameters
        ----------
        I0 : float
            Beam current in [A].

        Returns
        -------
        bool

        """
        return 2*self.Vc*np.sin(self.theta) + self.Vbr(I0)*np.sin(2*self.psi) > 0
    
    def plot_DC_Robinson_stability(self, detune_range = [-1e5,1e5]):
        """
        Plot DC Robinson stability limit.

        Parameters
        ----------
        detune_range : list or array, optional
            Range of tuning to plot in [Hz].

        Returns
        -------
        Figure.

        """
        old_detune = self.psi
        
        x = np.linspace(detune_range[0],detune_range[1],1000)
        y = []
        for i in range(0,x.size):
            self.detune = x[i]
            y.append(-self.Vc*(1+self.beta)/(self.Rs*np.sin(2*self.psi))*np.sin(self.theta)) # droite de stabilité
            
        fig = plt.figure()
        ax = plt.gca()
        ax.plot(x,y)
        ax.set_xlabel("Detune [Hz]")
        ax.set_ylabel("Threshold current [A]")
        ax.set_title("DC Robinson stability limit")
        
        self.psi = old_detune
        
        return fig
        
    def VRF(self, z, I0, F = 1, PHI = 0):
        """Total RF voltage taking into account form factor amplitude F and form factor phase PHI"""
        return self.Vg*np.cos(self.ring.k1*self.m*z + self.theta_g) - self.Vb(I0)*F*np.cos(self.ring.k1*self.m*z + self.psi - PHI)
    
    def dVRF(self, z, I0, F = 1, PHI = 0):
        """Return derivative of total RF voltage taking into account form factor amplitude F and form factor phase PHI"""
        return -1*self.Vg*self.ring.k1*self.m*np.sin(self.ring.k1*self.m*z + self.theta_g) + self.Vb(I0)*F*self.ring.k1*self.m*np.sin(self.ring.k1*self.m*z + self.psi - PHI)
    
    def ddVRF(self, z, I0, F = 1, PHI = 0):
        """Return the second derivative of total RF 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)
        
    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)