Source code for at.plot.resonances

"""AT plotting functions related to resonances."""

from __future__ import annotations

__all__ = ["farey_sequence", "plot_tune_diagram", "create_linepalette"]

import warnings
from fractions import Fraction

import matplotlib.axes
import matplotlib.pyplot as plt
import numpy as np

# 2024jul31 oblanco at ALBA CELLS


[docs] def create_linepalette( linestyle: str | dict = None, linecolor: str = None, linewidth: int = None, addtolabel: str = None, ) -> dict[str, any]: """Create a line palette to plot resonance lines. Parameters: linestyle: If a dictionary is passed, it should contain {"normal": style1, "skew": style2} If 'dots' it uses dotted styles as linestyles. Equivalent to: {"normal": "dashdot", "skew": "dotted"} Default: {"normal": '-', "skew": '--'} linecolor: line color, e.g. "k". Default: custom values. See :py:func:`plot_tune_diagram` linewidth: line width. Default: custom values. See :py:func:`plot_tune_diagram` addtolabel: adds a string to the line label Returns: palette: dictionary containing the line properties for resonance plots. """ # create default dictionary with line properties mypalettecolor1 = { 1: "k", 2: "b", 3: "r", 4: "g", 5: "m", 6: "c", 7: "y", 8: "darkcyan", 9: "lightgreen", 10: (0.1, 0.1, 0.1), 11: (0.1, 0.1, 0.1), 12: (0.2, 0.2, 0.2), 13: (0.3, 0.3, 0.3), 14: (0.4, 0.4, 0.4), 15: (0.5, 0.5, 0.5), } mypalettewidth_main = { 1: 4, 2: 3, 3: 2, 4: 1, 5: 1, 6: 1, 7: 1, 8: 1, 9: 1, 10: 1, 11: 1, 12: 1, 13: 1, 14: 1, 15: 1, } mypalettestyle_main = {"normal": "-", "skew": "--"} mypalettestyle_withdots = {"normal": "dashdot", "skew": "dotted"} # set the line style mypalettestyle = mypalettestyle_main if linestyle == "dots": mypalettestyle = mypalettestyle_withdots if isinstance(linestyle, dict): mypalettestyle = linestyle # set the line color mypalettecolor = mypalettecolor1 if linecolor is not None: for lsize in range(1, 16): mypalettecolor[lsize] = linecolor # set the line width mylinewidth = mypalettewidth_main if isinstance(linewidth, int) and linewidth > 0: for lsize in range(1, 16): mylinewidth[lsize] = linewidth # add to label if addtolabel is None: addtolabel = "" # set the dictionary linepropdict = {} resontypes = ["normal", "skew"] for resontype in resontypes: linepropdict[resontype] = {} for resontype in resontypes: for idx in range(1, 16): propaux = { "color": mypalettecolor[idx], "linestyle": mypalettestyle[resontype], "linewidth": mylinewidth[idx], "label": str(idx) + resontype[0] + addtolabel, } linepropdict[resontype][idx] = propaux return linepropdict
[docs] def farey_sequence(nthorder: int, verbose: bool = False) -> tuple[list, list]: """ Return the Farey sequence, and the resonance sequence of nth order. Parameters: nthorder: natural number bigger than 0. verbose: prints extra info. Default: False. Returns: fareyseqfloat: list of elements with the Farey sequence in Float format. See Eqs.(1,2,3) of [1]. fareyseqfrac: list of elements with the Farey sequence in Fraction format. See Eqs.(1,2,3) of [1]. Raises: ValueError: if given order is lower than 0, or window is zero. [1] R.Tomas. 'From Farey sequences to resonance diagrams. Phys.Rev.Acc.Beams 17, 014001 (2014)' """ # verboseprint to check flag only once verboseprint = print if verbose else lambda *a, **k: None if nthorder < 1: raise ValueError("The order must be a positive integer larger than zero.") verboseprint(f"nthorder={nthorder}") afarey = 0 bfarey = 1 cfarey = 1 dfarey = nthorder farey = [0, 1 / dfarey] fracfarey = [Fraction(0), Fraction(1, dfarey)] idx = 0 while (farey[-1] < 1) and (idx < 100): idx += 1 caux = np.floor((nthorder + bfarey) / dfarey) * cfarey - afarey daux = np.floor((nthorder + bfarey) / dfarey) * dfarey - bfarey afarey = cfarey bfarey = dfarey cfarey = int(caux) dfarey = int(daux) farey.append(cfarey / dfarey) fracfarey.append(Fraction(cfarey, dfarey)) verboseprint(f"farey_float{nthorder}= {farey}") verboseprint(f"farey_frac{nthorder} = {fracfarey}") return farey, fracfarey
[docs] def plot_tune_diagram( orders: int | tuple[int] = (1, 2, 3), periodicity: int = 1, window: list = (0, 1, 0, 1), verbose: bool = False, legend: bool = False, show: bool = True, block: bool = False, debug: bool = False, axes: matplotlib.axes.Axes = None, linestyle: dict or str = None, linecolor: str or any = None, linewidth: int = None, addtolabel: str = None, **kwargs: dict[str, any], ) -> tuple[matplotlib.axes.Axes, list, list]: r""" Plot the tune diagram and resonance lines for the given *orders*, *periodicity* and *window*. The resonance equation is :math:`a\nu_x + b\nu_y = c` with :math:`a,b` and :math:`c` integers. The order is: :math:`N=abs(a)+abs(b)`. Parameters: orders: integer or tuple of integers larger than zero. Default (1, 2, 3). periodicity: periodicity of the machine, integer larger than zero. Default: 1. window: ``(nux_min, nux_max, nuy_min, nuy_max)``: tuple of 4 values for the tune minimum and maximum window. Default: (0, 1, 0, 1). *window* is ignored if the parameter axes is given. verbose: print verbose output. legend: print legend on the plot. Default: False. show: show plot. Default: True. block: passed to plot.show(). Default: False. debug: extra output to check line construction. Default: False. axes: :py:class:`~matplotlib.axes.Axes` for plotting the resonances. If :py:obj:`None`, a new figure will be created. Note that if *axes* are given then *window* is ignored. linestyle: line style for normal and skew resonances. If a dictionary is passed, it should contain {"normal": style1, "skew": style2} If 'dots' it uses dotted styles as linestyles. Equivalent to: {"normal": "dashdot", "skew": "dotted"} Default: {"normal": '-', "skew": '--'} linecolor: single color for all resonances. Default: custom palette. See :ref:`Lines Color and Width <color_width>` linewidth: line width for all resonances. Default: custom values. See :ref:`Lines Color and Width <color_width>` addtolabel: adds a string to the line label, e.g. for the fourth order normal resonance "4n"+addtolabel Keyword Args: only (str): if 'normal', plot only normal resonances. if 'skew' plots only skew resonances. Otherwise, ignored. See the notes on Normal and Skew convention. linedict (dict): dictionary of custom line styles. See notes below. Returns: Axes (matplotlib.axes.Axes): object from matplotlib.axes._axes legend_h (list): list of handles for the legend legend_lab (list): list of labels for the legend NOTES: Normal and Skew convention: Line style is similar to reson.m from Matlab Middle Layer, MML, by L. Nadolski. Normal resonances are plotted with a continuous line. Skew resonances, i.e. N-abs(a) is odd, are plotted in dashed lines. .. _color_width: Lines Color and Width: Line style is similar to reson.m from Matlab Middle Layer, MML, by L. Nadolski. 1st: black, width 4 2nd: blue, width 3 3rd: red, width 2 4th: green, width 1 5th: magenta, witdh 1 6th: cyan, width 1 7th: yellow, width 1 8th: darkcyan, width 1 9th: 'lightgreen', width 1 10th to 15th: RGB increased in steps of 0.1, width 1 Custom Style: You could pass a custom line style in a dictionary as ``linedict=mydictionary``, where mydictionary should contain two entries: dict("normal": normald, "skew": skewd). normald and skewd are also dictionaries, each entry contains as key the resonance order and as value the line properties to use in the plot. The default dictionary is created with :py:func:`create_linepalette` mydictionary = at.plot.resonances.create_linepalette() you could edit the needed entries. Raises: ValueError: if given resonances are lower than 0, or window is zero. """ # verboseprint to check flag only once verboseprint = print if verbose else lambda *a, **k: None # print debugging output, equivalent to extra verbose debugprint = print if debug else lambda *a, **k: None # only plot normal or skew normalskew = [0, 1] onlyns = kwargs.pop("only", False) if onlyns == "normal": normalskew = [0] if onlyns == "skew": normalskew = [1] # orders could be a single int if isinstance(orders, int): orders = [orders] # check that all are larger than 0 if sum(n < 1 for n in orders): raise ValueError("Negative resonances are not allowed.") if periodicity < 1: raise ValueError("Period must be a positive integer.") verboseprint(f"The periodicity is {periodicity}") verboseprint(f"The window is {window}") # check the window windowa = np.array(window) if windowa[0] == windowa[1]: raise ValueError("horizontal coordinates must be different") if windowa[2] == windowa[3]: raise ValueError("vertical coordinates must be different") if windowa[1] < windowa[0]: windowa[0], windowa[1] = windowa[1], windowa[0] warnings.warn("Swapping horizontal coordinates", stacklevel=1) if windowa[3] < windowa[2]: windowa[2], windowa[3] = windowa[3], windowa[2] warnings.warn("Swapping vertical coordinates", stacklevel=1) # get xlimits and ylimits the_axeslims = windowa.reshape((2, 2)) maxreson2calc = np.max(orders) verboseprint(f"Farey max order={maxreson2calc}") # get the Farey collection, i.e., a list of farey sequences, one per order fareycollectionfloat = {} fareycollectionfrac = {} for nthorder in range(1, maxreson2calc + 1): farey, fracfarey = farey_sequence(nthorder) fareycollectionfloat[nthorder] = farey fareycollectionfrac[nthorder] = fracfarey verboseprint(f"the Farey collection is {fareycollectionfloat}") # plot configuration if axes is None: fig = plt.figure() axes = fig.add_subplot(111) else: verboseprint("Axes already exist, ignore window") the_axeslims = np.array([axes.get_xlim(), axes.get_ylim()]) # min/max to plot lines with slopes minx = np.floor(the_axeslims[0, 0]) minx = minx - periodicity - np.mod(minx, periodicity) maxx = np.ceil(the_axeslims[0, 1]) maxx = maxx + periodicity - np.mod(maxx, periodicity) minmaxxdist = maxx - minx miny = np.floor(the_axeslims[1, 0]) miny = miny - periodicity - np.mod(miny, periodicity) maxy = np.ceil(the_axeslims[1, 1]) maxy = maxy + periodicity - np.mod(maxy, periodicity) minmaxydist = maxy - miny lprop = create_linepalette( linestyle=linestyle, linecolor=linecolor, linewidth=linewidth, addtolabel=addtolabel, ) userprop = kwargs.pop("linedict", {}) lprop["normal"].update(userprop.get("normal", {})) lprop["skew"].update(userprop.get("skew", {})) # we only need two points to define a line nauxpoints = 2 # window min/max,horizontal and vertical axes.set_xlim(the_axeslims[0, :]) axes.set_ylim(the_axeslims[1, :]) # start to check the Farey collection, starting with 0 collectaux1 = [0] idxtotype = {0: "normal", 1: "skew"} for nthorder in range(1, maxreson2calc + 1): debugprint(f"nthorder={nthorder}") collectaux2 = fareycollectionfloat[nthorder] thesteps = list(set(collectaux2) - set(collectaux1)) debugprint(thesteps) chosenstep = min(thesteps) debugprint(f"chosenstep={chosenstep}") collectaux1 = collectaux2 if nthorder in orders: # increase step by the periodicity in straight resonances debugprint("enter plotting horizontal straight lines") if 0 in normalskew: for iaux in np.arange(minx, maxx + 0.000001, periodicity * chosenstep): axes.axvline(x=iaux, **lprop["normal"][nthorder]) debugprint("enter plotting vertical straight lines") nsaux = np.mod(nthorder, 2) if nsaux in normalskew: for iaux in np.arange(miny, maxy + 0.000001, periodicity * chosenstep): axes.axhline(y=iaux, **lprop[idxtotype[nsaux]][nthorder]) # aeq*nux + beq*nuy = nthorder for aeq in range(1, nthorder): debugprint(f"enter plotting diagonals {aeq}") beq = nthorder - aeq chosenslope = 1.0 * aeq / beq ystep = 1.0 / beq debugprint(f"chosen slope={chosenslope}") debugprint(f"chosen ystep={ystep}") # calculate the variation on the vertical direction from window size yyaux = np.ceil(minmaxxdist * chosenslope + minmaxydist * ystep) y1aux = -yyaux + miny y2aux = yyaux + maxy # adapt to periodicity y1aux = periodicity * np.floor(y1aux / periodicity) y2aux = periodicity * np.ceil(y2aux / periodicity) debugprint(f"minx={minx},maxx={maxx},minmaxxdist={minmaxxdist}") debugprint(f"miny={miny},maxy={maxy},minmaxydist={minmaxydist}") debugprint(f"y1aux={y1aux},y2aux={y2aux}") xaux = np.linspace(minx, maxx, nauxpoints) debugprint(f"xaux={xaux}") nsaux = np.mod(beq, 2) if nsaux in normalskew: for istep in np.arange(0, y2aux - y1aux + 0.0001, ystep): y1line = ( -chosenslope * (xaux - minx) + y2aux - periodicity * istep ) y2line = ( chosenslope * (xaux - minx) + y1aux + periodicity * istep ) debugprint(f"y1line={y1line},y2line={y2line}") axes.plot(xaux, y1line, **lprop[idxtotype[nsaux]][nthorder]) axes.plot(xaux, y2line, **lprop[idxtotype[nsaux]][nthorder]) # include labels axes.set_xlabel(r"$\nu_x$") axes.set_ylabel(r"$\nu_y$") # printing legend if necessary handleall, labelall = axes.get_legend_handles_labels() hstyle = [hline._linestyle for hline in handleall] joinlabelstyle = [i + j for i, j in zip(labelall, hstyle)] uniquelabelstyle = sorted(set(joinlabelstyle)) idxunique = [joinlabelstyle.index(uniqele) for uniqele in uniquelabelstyle] myleghandles = [handleall[idx] for idx in idxunique] myleglabels = [labelall[idx] for idx in idxunique] if legend: axes.legend(handles=myleghandles, labels=myleglabels, frameon=legend) if show: plt.show(block=block) return axes, myleghandles, myleglabels