22Coupling
33********
44
5- Functions to estimate coupling from twiss dataframes and
6- different methods to calculate the closest tune approach from
7- the calculated coupling RDTs.
8-
5+ Functions to estimate coupling from twiss dataframes and different methods to calculate the closest tune
6+ approach from the calculated coupling RDTs.
97"""
108import logging
119from contextlib import suppress
12- from typing import Sequence
10+ from typing import Sequence , Tuple
1311
1412import numpy as np
1513from pandas import DataFrame , Series
1614from tfs import TfsDataFrame
1715
1816from optics_functions .constants import (ALPHA , BETA , GAMMA , X , Y , TUNE , DELTA ,
19- MINIMUM , PI2 , PHASE_ADV , S , LENGTH , IMAG , REAL )
17+ MINIMUM , PI2 , PHASE_ADV , S , LENGTH ,
18+ IMAG , REAL , F1010 , F1001 )
2019from optics_functions .rdt import calculate_rdts
2120from optics_functions .utils import split_complex_columns , timeit
2221
23- COUPLING_RDTS = ["F1001" , "F1010" ]
24-
25-
22+ COUPLING_RDTS = [F1001 , F1010 ]
2623LOG = logging .getLogger (__name__ )
2724
2825
@@ -38,7 +35,7 @@ def coupling_via_rdts(df: TfsDataFrame, complex_columns: bool = True, **kwargs)
3835 calculations from [CalagaBetatronCoupling2005]_ .
3936
4037 Args:
41- df (TfsDataFrame): Twiss Dataframe
38+ df (TfsDataFrame): Twiss Dataframe.
4239 complex_columns (bool): Output complex values in single column of type complex.
4340 If ``False``, split complex columns into
4441 two real-valued columns.
@@ -49,11 +46,13 @@ def coupling_via_rdts(df: TfsDataFrame, complex_columns: bool = True, **kwargs)
4946 and ``hamiltionian_terms``.
5047
5148 Returns:
52- New TfsDataFrame with Coupling Columns.
49+ A new `` TfsDataFrame`` with Coupling Columns.
5350 """
5451 df_res = calculate_rdts (df , rdts = COUPLING_RDTS , ** kwargs )
5552 for rdt in COUPLING_RDTS :
56- df_res .loc [:, rdt ].to_numpy ().real *= - 1 # definition, also: sets value in dataframe
53+ rdt_array = df_res [rdt ].to_numpy () # might return a copy!
54+ rdt_array .real *= - 1 # definition
55+ df_res .loc [:, rdt ] = rdt_array
5756
5857 if not complex_columns :
5958 df_res = split_complex_columns (df_res , COUPLING_RDTS )
@@ -110,10 +109,10 @@ def coupling_via_cmatrix(df: DataFrame, complex_columns: bool = True,
110109 if "rdts" in output :
111110 # Eq. (9) and Eq. (10)
112111 denom = 1 / (4 * gamma )
113- df_res .loc [:, " F1001" ] = denom * (+ c [:, 0 , 1 ] - c [:, 1 , 0 ] + (c [:, 0 , 0 ] + c [:, 1 , 1 ]) * 1j )
114- df_res .loc [:, " F1010" ] = denom * (- c [:, 0 , 1 ] - c [:, 1 , 0 ] + (c [:, 0 , 0 ] - c [:, 1 , 1 ]) * 1j )
115- LOG .info (f"Average coupling amplitude |F1001|: { df_res [' F1001' ].abs ().mean ():g} " )
116- LOG .info (f"Average coupling amplitude |F1010|: { df_res [' F1010' ].abs ().mean ():g} " )
112+ df_res .loc [:, F1001 ] = denom * (+ c [:, 0 , 1 ] - c [:, 1 , 0 ] + (c [:, 0 , 0 ] + c [:, 1 , 1 ]) * 1j )
113+ df_res .loc [:, F1010 ] = denom * (- c [:, 0 , 1 ] - c [:, 1 , 0 ] + (c [:, 0 , 0 ] - c [:, 1 , 1 ]) * 1j )
114+ LOG .info (f"Average coupling amplitude |F1001|: { df_res [F1001 ].abs ().mean ():g} " )
115+ LOG .info (f"Average coupling amplitude |F1010|: { df_res [F1010 ].abs ().mean ():g} " )
117116
118117 if not complex_columns :
119118 df_res = split_complex_columns (df_res , COUPLING_RDTS )
@@ -141,13 +140,13 @@ def rmatrix_from_coupling(df: DataFrame, complex_columns: bool = True) -> DataFr
141140 See [CalagaBetatronCoupling2005]_ .
142141
143142 Args:
144- df (DataFrame): Twiss Dataframe
143+ df (DataFrame): Twiss Dataframe.
145144 complex_columns (bool): Tells the function if the coupling input columns
146145 are complex-valued or split into real and
147146 imaginary parts.
148147
149148 Returns:
150- New DataFrame containing the R-columns.
149+ A new `` DataFrame`` containing the R-columns.
151150 """
152151 LOG .info ("Calculating r-matrix from coupling rdts." )
153152 df_res = DataFrame (index = df .index )
@@ -174,19 +173,19 @@ def rmatrix_from_coupling(df: DataFrame, complex_columns: bool = True) -> DataFr
174173
175174 # Eq. (15)
176175 if complex_columns :
177- abs_squared_diff = df [" F1001" ].abs ()** 2 - df [" F1010" ].abs ()** 2
176+ abs_squared_diff = df [F1001 ].abs ()** 2 - df [F1010 ].abs ()** 2
178177 else :
179- abs_squared_diff = (df [f"F1001{ REAL } " ]** 2 + df [f"F1001{ IMAG } " ]** 2 -
180- df [f"F1010{ REAL } " ]** 2 - df [f"F1010{ IMAG } " ]** 2 )
178+ abs_squared_diff = (df [f"{ F1001 } { REAL } " ]** 2 + df [f"{ F1001 } { IMAG } " ]** 2 -
179+ df [f"{ F1010 } { REAL } " ]** 2 - df [f"{ F1010 } { IMAG } " ]** 2 )
181180
182181 gamma = np .sqrt (1.0 / (1.0 + 4.0 * abs_squared_diff ))
183182
184183 # Eq. (11) and Eq. (12)
185184 cbar = np .zeros ((n , 2 , 2 ))
186- cbar [:, 0 , 0 ] = (df [f"F1001{ IMAG } " ] + df [f"F1010{ IMAG } " ]).to_numpy ()
187- cbar [:, 0 , 1 ] = - (df [f"F1010{ REAL } " ] - df [f"F1001{ REAL } " ]).to_numpy ()
188- cbar [:, 1 , 0 ] = - (df [f"F1010{ REAL } " ] + df [f"F1001{ REAL } " ]).to_numpy ()
189- cbar [:, 1 , 1 ] = (df [f"F1001{ IMAG } " ] - df [f"F1010{ IMAG } " ]).to_numpy ()
185+ cbar [:, 0 , 0 ] = (df [f"{ F1001 } { IMAG } " ] + df [f"{ F1010 } { IMAG } " ]).to_numpy ()
186+ cbar [:, 0 , 1 ] = - (df [f"{ F1010 } { REAL } " ] - df [f"{ F1001 } { REAL } " ]).to_numpy ()
187+ cbar [:, 1 , 0 ] = - (df [f"{ F1010 } { REAL } " ] + df [f"{ F1001 } { REAL } " ]).to_numpy ()
188+ cbar [:, 1 , 1 ] = (df [f"{ F1001 } { IMAG } " ] - df [f"{ F1010 } { IMAG } " ]).to_numpy ()
190189 cbar = 2 * gamma .to_numpy ()[:, None , None ] * cbar
191190
192191 # Gx^-1 * Cbar * Gy = C (Eq. (5) inverted)
@@ -210,43 +209,57 @@ def rmatrix_from_coupling(df: DataFrame, complex_columns: bool = True) -> DataFr
210209
211210# Closest Tune Approach --------------------------------------------------------
212211
213- def closest_tune_approach (df : TfsDataFrame , qx : float = None , qy : float = None ,
214- method : str = "calaga" ) -> TfsDataFrame :
212+ def closest_tune_approach (
213+ df : TfsDataFrame , qx : float = None , qy : float = None , method : str = "teapot"
214+ ) -> TfsDataFrame :
215215 """Calculates the closest tune approach from coupling resonances.
216216
217217 A complex F1001 column is assumed to be present in the DataFrame.
218218 This can be calculated by :func:`~optics_functions.rdt.rdts`
219219 :func:`~optics_functions.coupling.coupling_from_rdts` or
220220 :func:`~optics_functions.coupling.coupling_from_cmatrix`.
221- If F1010 is also present it is used, otherwise assumed 0.
221+ If F1010 is also present it is used, otherwise it is assumed 0.
222222
223223 The closest tune approach is calculated by means of Eq. (27) in
224- [CalagaBetatronCoupling2005]_ (method='calaga') by default,
225- or approximated by Eq. (1) in [PerssonImprovedControlCoupling2014]_
226- (method='franchi') or Eq. (2) in [PerssonImprovedControlCoupling2014]_
227- (method='persson') or the latter without the exp(i(Qx-Qy)s/R) term
228- (method='persson_alt').
229-
230- For the 'persson' and 'persson_alt' methods, also MUX and MUY columns
224+ [CalagaBetatronCoupling2005]_ (method="teapot" or "calaga") by default,
225+ or approximated by
226+ Eq. (1) in [PerssonImprovedControlCoupling2014]_ (method="franchi"),
227+ Eq. (27) in [CalagaBetatronCoupling2005]_ with the Franchi appoximation (method="teapot_franchi"),
228+ Eq. (2) in [PerssonImprovedControlCoupling2014]_ (method="persson"),
229+ the latter without the exp(i(Qx-Qy)s/R) term (method="persson_alt"),
230+ Eq. (14) in [HoydalsvikEvaluationOfTheClosestTuneApproach2021]_ (method="hoydalsvik"),
231+ or the latter without the exp(i(Qx-Qy)s/R) term (method="hoydalsvik_alt").
232+
233+ For "persson[_alt]" and "hoydalsvik[_alt]" methods, also MUX and MUY columns
231234 are needed in the DataFrame as well as LENGTH (of the machine) and S column
232- for the ' persson' method .
235+ for the " persson" and "hoydalsvik" methods .
233236
234237 Args:
235238 df (TfsDataFrame): Twiss Dataframe, needs to have complex-valued F1001 column.
236239 qx (float): Tune in X-Plane (if not given, header df.Q1 is assumed present).
237240 qy (float): Tune in Y-Plane (if not given, header df.Q2 is assumed present).
238241 method (str): Which method to use for evaluation.
239- Choices: 'calaga', 'franchi', 'persson' and 'persson_alt'.
242+ Choices: "calaga", "teapot", "franchi", "teapot_franchi",
243+ "persson", "persson_alt", "hoydalsvik" or "hoydalsvik_alt".
240244
241245 Returns:
242- New TfsDataFrame with closest tune approach (DELTAQMIN) column.
243- The value is real for 'calaga' and 'franchi' methods,
246+ A new ``TfsDataFrame`` with a closest tune approach (DELTAQMIN) column.
247+ The value is real for "calaga", "teapot", "teapot_franchi" and "franchi"
248+ methods. The actual closest tune approach value is the absolute value
249+ of the mean of this column.
244250 """
251+ if F1001 not in df .columns :
252+ raise KeyError (f"'{ F1001 } ' column not in dataframe. Needed to calculated closest tune approach." )
253+
245254 method_map = {
246- "calaga" : _cta_calaga ,
255+ "teapot" : _cta_teapot , # as named in [HoydalsvikEvaluationOfTheClosestTuneApproach2021]_
256+ "calaga" : _cta_teapot , # for compatibility reasons
257+ "teapot_franchi" : _cta_teapot_franchi ,
247258 "franchi" : _cta_franchi ,
248259 "persson" : _cta_persson ,
249260 "persson_alt" : _cta_persson_alt ,
261+ "hoydalsvik" : _cta_hoydalsvik ,
262+ "hoydalsvik_alt" : _cta_hoydalsvik_alt ,
250263 }
251264 if qx is None :
252265 qx = df .headers [f"{ TUNE } 1" ]
@@ -255,44 +268,111 @@ def closest_tune_approach(df: TfsDataFrame, qx: float = None, qy: float = None,
255268
256269 qx_frac , qy_frac = qx % 1 , qy % 1
257270
271+ check_resonance_relation (df )
272+
258273 dqmin_str = f"{ DELTA } { TUNE } { MINIMUM } "
259274 df_res = TfsDataFrame (index = df .index , columns = [dqmin_str ])
260275 df_res [dqmin_str ] = method_map [method .lower ()](df , qx_frac , qy_frac )
261276
262- LOG .info (f"({ method } ) |C-| = { np .abs (df_res [dqmin_str ].mean ())} " )
277+ LOG .info (f"({ method . lower () } ) |C-| = { np .abs (df_res [dqmin_str ]. dropna () .mean ())} " )
263278 return df_res
264279
265280
266281def _cta_franchi (df : TfsDataFrame , qx_frac : float , qy_frac : float ) -> Series :
267282 """ Closest tune approach calculated by Eq. (1) in [PerssonImprovedControlCoupling2014]_ . """
268- return 4 * (qx_frac - qy_frac ) * df [" F1001" ].abs ()
283+ return 4 * (qx_frac - qy_frac ) * df [F1001 ].abs ()
269284
270285
271286def _cta_persson_alt (df : TfsDataFrame , qx_frac : float , qy_frac : float ) -> Series :
272287 """Closest tune approach calculated by Eq. (2) in [PerssonImprovedControlCoupling2014]_ .
273288 The exp(i(Qx-Qy)s/R) term is omitted.
274289 """
275290 deltaq = qx_frac - qy_frac # fractional tune split
276- return 4 * deltaq * df ["F1001" ] * np .exp (- 1j * (df [f"{ PHASE_ADV } { X } " ] - df [f"{ PHASE_ADV } { Y } " ]))
291+ length_weights = _get_weights_from_lengths (df )
292+ phase_diff = df [f"{ PHASE_ADV } { X } " ] - df [f"{ PHASE_ADV } { Y } " ]
293+ return 4 * deltaq * length_weights * df [F1001 ] * np .exp (- 1j * PI2 * phase_diff )
277294
278295
279296def _cta_persson (df : TfsDataFrame , qx_frac : float , qy_frac : float ) -> Series :
280297 """ Closest tune approach calculated by Eq. (2) in [PerssonImprovedControlCoupling2014]_ . """
281298 deltaq = qx_frac - qy_frac # fractional tune split
282- exponential_term = ((deltaq * df [S ] / (df .headers [LENGTH ] / PI2 )) - (df [f"{ PHASE_ADV } { X } " ] - df [f"{ PHASE_ADV } { Y } " ]))
283- return 4 * deltaq * df ['F1001' ] * np .exp (1j * exponential_term )
299+ location_term = np .exp (1j * PI2 * (deltaq * df [S ] / (df .headers [LENGTH ] / PI2 )))
300+ return _cta_persson_alt (df , qx_frac , qy_frac ) * location_term
301+
302+
303+ def _cta_hoydalsvik (df : TfsDataFrame , qx_frac : float , qy_frac : float ) -> Series :
304+ """ Closest tune approach calculated by Eq. (14) in
305+ [HoydalsvikEvaluationOfTheClosestTuneApproach2021]_ .
306+ This is like the persson estimate but divided by 1 + 4|F1001|^2 ."""
307+ return _cta_persson (df , qx_frac , qy_frac ) / (1 + 4 * df [F1001 ].abs () ** 2 )
284308
285309
286- def _cta_calaga (df : TfsDataFrame , qx_frac : float , qy_frac : float ) -> Series :
310+ def _cta_hoydalsvik_alt (df : TfsDataFrame , qx_frac : float , qy_frac : float ) -> Series :
311+ """ Closest tune approach calculated by Eq. (14) without the s-term in
312+ [HoydalsvikEvaluationOfTheClosestTuneApproach2021]_ .
313+ This is like the persson_alt estimate but divided by 1 + 4|F1001|^2 ."""
314+ return _cta_persson_alt (df , qx_frac , qy_frac ) / (1 + 4 * df [F1001 ].abs () ** 2 )
315+
316+
317+ def _cta_teapot (df : TfsDataFrame , qx_frac : float , qy_frac : float ) -> Series :
287318 """Closest tune approach calculated by Eq. (27) in [CalagaBetatronCoupling2005]_ .
288319 If F1010 is not given, it is assumed to be zero.
289320 """
290- f_diff = df [" F1001" ].abs () ** 2
291- with suppress (KeyError ):
292- f_diff -= df ["1010" ].abs () ** 2
321+ f_diff = df [F1001 ].abs () ** 2
322+ with suppress (KeyError ): # this is the only estimate that uses sum resonance
323+ f_diff -= df [F1010 ].abs () ** 2
293324
294325 return (
295326 (np .cos (PI2 * qx_frac ) - np .cos (PI2 * qy_frac ))
296327 / (np .pi * (np .sin (PI2 * qx_frac ) + np .sin (PI2 * qy_frac )))
297328 * (4 * np .sqrt (f_diff ) / (1 + 4 * f_diff ))
298329 )
330+
331+
332+ def _cta_teapot_franchi (df : TfsDataFrame , qx_frac : float , qy_frac : float ) -> Series :
333+ """Closest tune approach calculated by Eq. (12) in
334+ [HoydalsvikEvaluationOfTheClosestTuneApproach2021]_ .
335+ This is the teapot approach with the Franchi approximation. """
336+ return 4 * (qx_frac - qy_frac ) * df [F1001 ].abs () / (1 + 4 * df [F1001 ].abs () ** 2 )
337+
338+
339+ def _get_weights_from_lengths (df : TfsDataFrame ) -> Tuple [float , np .array ]:
340+ """Coefficients for the `persson` method. """
341+ # approximate length of each element (ds in integral)
342+ s_periodic = np .zeros (len (df ) + 1 )
343+ s_periodic [1 :] = df [S ].to_numpy ()
344+ s_periodic [0 ] = df [S ][- 1 ] - df .headers [LENGTH ]
345+
346+ # weight ds/(2*pi*R) * N (as we take the mean afterwards)
347+ weights = np .diff (s_periodic ) / df .headers [LENGTH ] * len (df )
348+ return weights
349+
350+
351+ def check_resonance_relation (df : DataFrame , to_nan : bool = False ) -> DataFrame :
352+ """Checks that |F1001| >= |F1010|.
353+ If desired, sets the invalid points to NaN. This is only used for checking
354+ in the :func:`~optics_functions.coupling.closest_tune_approach` function,
355+ but can be invoked by the user with ``to_nan = True`` and the resulting
356+ DataFrame can then be passed to
357+ :func:`~optics_functions.coupling.closest_tune_approach`.
358+
359+ Args:
360+ df (DataFrame): Dataframe containing the coupling columns.
361+ to_nan (bool): If true, sets values where |F1001| <= |F1010| to ``NaN``.
362+
363+ Returns:
364+ A copy of the input data frame, with or without NaNs.
365+ """
366+ df = df .copy ()
367+ if F1010 not in df .columns :
368+ LOG .debug ("Sum-resonance not in df, skipping resonance relation check." )
369+ return df
370+
371+ condition_not_fulfilled = df [F1001 ].abs () < df [F1010 ].abs () # comparison with NaN always yields False
372+ if any (condition_not_fulfilled ):
373+ LOG .warning (f"In { sum (condition_not_fulfilled ) / len (df .index ) * 100 } % "
374+ "of the data points |F1001| < |F1010|. Your closest tune "
375+ "approach estimates might not be accurate." )
376+ if to_nan :
377+ df .loc [condition_not_fulfilled , COUPLING_RDTS ] = np .NaN
378+ return df
0 commit comments