Composable cycles

Version:

0.12

Datum:

18. Januar 2024

Docs

https://matplotlib.de/cycler

PyPI

https://pypi.python.org/pypi/Cycler

GitHub

https://github.com/matplotlib/cycler

cycler API

cycler()

Erstellt ein neues Cycler-Objekt aus einem einzelnen Positionsargument, einem Paar von Positionsargumenten oder der Kombination von Schlüsselwortargumenten.

Cycler(links[, rechts, op])

Zusammensetzbare Zyklen.

concat(links, rechts)

Verkettet Cyclers, als wären sie über itertools.chain verkettet worden.

Die öffentliche API von cycler besteht aus einer Klasse Cycler, einer Factory-Funktion cycler() und einer Verkettungsfunktion concat(). Die Factory-Funktion bietet eine einfache Schnittstelle zum Erstellen von „Basis“-Cycler-Objekten, während die Klasse sich um die Kompositions- und Iterationslogik kümmert.

Cycler Verwendung

Basis

Ein einzelnes Cycler-Objekt kann verwendet werden, um einfach über einen einzelnen Stil zu iterieren. Um den Cycler zu erstellen, verwenden Sie die Funktion cycler(), um einen Schlüssel/Stil/Schlüsselwortargument an eine Reihe von Werten zu koppeln. Der Schlüssel muss hashbar sein (da er schließlich als Schlüssel in einem dict verwendet wird).

In [1]: from __future__ import print_function

In [2]: from cycler import cycler

In [3]: color_cycle = cycler(color=['r', 'g', 'b'])

In [4]: color_cycle
Out[4]: cycler('color', ['r', 'g', 'b'])

Der Cycler kennt seine Länge und seine Schlüssel

In [5]: len(color_cycle)
Out[5]: 3

In [6]: color_cycle.keys
Out[6]: {'color'}

Die Iteration über dieses Objekt liefert eine Reihe von dict-Objekten, die mit dem Label indiziert sind

In [7]: for v in color_cycle:
   ...:     print(v)
   ...: 
{'color': 'r'}
{'color': 'g'}
{'color': 'b'}

Cycler-Objekte können als Argument an cycler() übergeben werden, was einen neuen Cycler mit einem neuen Label, aber denselben Werten zurückgibt.

In [8]: cycler(ec=color_cycle)
Out[8]: cycler('ec', ['r', 'g', 'b'])

Die Iteration über einen Cycler ergibt die endliche Liste von Einträgen. Um einen unendlichen Zyklus zu erhalten, rufen Sie das Cycler-Objekt auf (wie einen Generator)

In [9]: cc = color_cycle()

In [10]: for j, c in zip(range(5),  cc):
   ....:     print(j, c)
   ....: 
0 {'color': 'r'}
1 {'color': 'g'}
2 {'color': 'b'}
3 {'color': 'r'}
4 {'color': 'g'}

Komposition

Ein einzelner Cycler kann genauso leicht durch eine einzelne for-Schleife ersetzt werden. Die Stärke von Cycler-Objekten liegt darin, dass sie zusammengesetzt werden können, um einfach komplexe Mehrfachschlüsselzyklen zu erstellen.

Addition

Cycler mit gleicher Länge und unterschiedlichen Schlüsseln können addiert werden, um das „innere“ Produkt zweier Zyklen zu erhalten

In [11]: lw_cycle = cycler(lw=range(1, 4))

In [12]: wc = lw_cycle + color_cycle

Das Ergebnis hat die gleiche Länge und Schlüssel, die die Vereinigung der beiden Eingabe-Cycler sind.

In [13]: len(wc)
Out[13]: 3

In [14]: wc.keys
Out[14]: {'color', 'lw'}

und die Iteration über das Ergebnis ist die Kombination der beiden Eingabezyklen

In [15]: for s in wc:
   ....:     print(s)
   ....: 
{'lw': 1, 'color': 'r'}
{'lw': 2, 'color': 'g'}
{'lw': 3, 'color': 'b'}

Wie bei der Arithmetik ist die Addition kommutativ

In [16]: lw_c = lw_cycle + color_cycle

In [17]: c_lw = color_cycle + lw_cycle

In [18]: for j, (a, b) in enumerate(zip(lw_c, c_lw)):
   ....:    print('({j}) A: {A!r} B: {B!r}'.format(j=j, A=a, B=b))
   ....: 
(0) A: {'lw': 1, 'color': 'r'} B: {'color': 'r', 'lw': 1}
(1) A: {'lw': 2, 'color': 'g'} B: {'color': 'g', 'lw': 2}
(2) A: {'lw': 3, 'color': 'b'} B: {'color': 'b', 'lw': 3}

Zur Vereinfachung kann die Funktion cycler() mehrere Schlüssel-Wert-Paare entgegennehmen und sie automatisch durch Addition zu einem einzigen Cycler zusammensetzen

In [19]: wc = cycler(c=['r', 'g', 'b'], lw=range(3))

In [20]: for s in wc:
   ....:     print(s)
   ....: 
{'c': 'r', 'lw': 0}
{'c': 'g', 'lw': 1}
{'c': 'b', 'lw': 2}

Multiplikation

Jedes Paar von Cycler kann multipliziert werden

In [21]: m_cycle = cycler(marker=['s', 'o'])

In [22]: m_c = m_cycle * color_cycle

was das „äußere Produkt“ der beiden Zyklen ergibt (wie bei itertools.product() )

In [23]: len(m_c)
Out[23]: 6

In [24]: m_c.keys
Out[24]: {'color', 'marker'}

In [25]: for s in m_c:
   ....:     print(s)
   ....: 
{'marker': 's', 'color': 'r'}
{'marker': 's', 'color': 'g'}
{'marker': 's', 'color': 'b'}
{'marker': 'o', 'color': 'r'}
{'marker': 'o', 'color': 'g'}
{'marker': 'o', 'color': 'b'}

Beachten Sie, dass die Multiplikation im Gegensatz zur Addition nicht kommutativ ist (wie bei Matrizen)

In [26]: c_m = color_cycle * m_cycle

In [27]: for j, (a, b) in enumerate(zip(c_m, m_c)):
   ....:    print('({j}) A: {A!r} B: {B!r}'.format(j=j, A=a, B=b))
   ....: 
(0) A: {'color': 'r', 'marker': 's'} B: {'marker': 's', 'color': 'r'}
(1) A: {'color': 'r', 'marker': 'o'} B: {'marker': 's', 'color': 'g'}
(2) A: {'color': 'g', 'marker': 's'} B: {'marker': 's', 'color': 'b'}
(3) A: {'color': 'g', 'marker': 'o'} B: {'marker': 'o', 'color': 'r'}
(4) A: {'color': 'b', 'marker': 's'} B: {'marker': 'o', 'color': 'g'}
(5) A: {'color': 'b', 'marker': 'o'} B: {'marker': 'o', 'color': 'b'}

Ganzzahlige Multiplikation

Cycler können auch mit Ganzzahlen multipliziert werden, um die Länge zu erhöhen.

In [28]: color_cycle * 2
Out[28]: cycler('color', ['r', 'g', 'b', 'r', 'g', 'b'])

In [29]: 2 * color_cycle
Out[29]: cycler('color', ['r', 'g', 'b', 'r', 'g', 'b'])

Verkettung

Cycler-Objekte können entweder über die Methode Cycler.concat()

In [30]: color_cycle.concat(color_cycle)
Out[30]: cycler('color', ['r', 'g', 'b', 'r', 'g', 'b'])

oder die Top-Level-Funktion concat() verkettet werden

In [31]: from cycler import concat

In [32]: concat(color_cycle, color_cycle)
Out[32]: cycler('color', ['r', 'g', 'b', 'r', 'g', 'b'])

Slicing

Zyklen können mit slice-Objekten geschnitten werden

In [33]: color_cycle[::-1]
Out[33]: cycler('color', ['b', 'g', 'r'])

In [34]: color_cycle[:2]
Out[34]: cycler('color', ['r', 'g'])

In [35]: color_cycle[1:]
Out[35]: cycler('color', ['g', 'b'])

um eine Teilmenge des Zyklus als neuen Cycler zurückzugeben.

Inspektion des Cycler

Um die Werte des transponierten Cycler zu inspizieren, verwenden Sie die Methode Cycler.by_key

In [36]: c_m.by_key()
Out[36]: 
{'marker': ['s', 'o', 's', 'o', 's', 'o'],
 'color': ['r', 'r', 'g', 'g', 'b', 'b']}

Dieses dict kann mutiert und zum Erstellen eines neuen Cycler mit den aktualisierten Werten verwendet werden

In [37]: bk = c_m.by_key()

In [38]: bk['color'] = ['green'] * len(c_m)

In [39]: cycler(**bk)
Out[39]: (cycler('marker', ['s', 'o', 's', 'o', 's', 'o']) + cycler('color', ['green', 'green', 'green', 'green', 'green', 'green']))

Beispiele

Wir können Cycler-Instanzen verwenden, um über ein oder mehrere kwarg für plot zu iterieren

from cycler import cycler
from itertools import cycle

fig, (ax1, ax2) = plt.subplots(1, 2, tight_layout=True,
                               figsize=(8, 4))
x = np.arange(10)

color_cycle = cycler(c=['r', 'g', 'b'])

for i, sty in enumerate(color_cycle):
   ax1.plot(x, x*(i+1), **sty)


for i, sty in zip(range(1, 5), cycle(color_cycle)):
   ax2.plot(x, x*i, **sty)

(Quellcode, png, hires.png, pdf)

_images/index-1.png
from cycler import cycler
from itertools import cycle

fig, (ax1, ax2) = plt.subplots(1, 2, tight_layout=True,
                               figsize=(8, 4))
x = np.arange(10)

color_cycle = cycler(c=['r', 'g', 'b'])
ls_cycle = cycler('ls', ['-', '--'])
lw_cycle = cycler('lw', range(1, 4))

sty_cycle = ls_cycle * (color_cycle + lw_cycle)

for i, sty in enumerate(sty_cycle):
   ax1.plot(x, x*(i+1), **sty)

sty_cycle = (color_cycle + lw_cycle) * ls_cycle

for i, sty in enumerate(sty_cycle):
   ax2.plot(x, x*(i+1), **sty)

(Quellcode, png, hires.png, pdf)

_images/index-2.png

Persistente Zyklen

Es kann nützlich sein, einem gegebenen Label über ein Wörterbuch-Lookup einen Stil zuzuordnen und diese Zuordnung dynamisch zu generieren. Dies kann leicht mit einem defaultdict erreicht werden

In [40]: from cycler import cycler as cy

In [41]: from collections import defaultdict

In [42]: cyl = cy('c', 'rgb') + cy('lw', range(1, 4))

Um eine endliche Menge von Stilen zu erhalten

In [43]: finite_cy_iter = iter(cyl)

In [44]: dd_finite = defaultdict(lambda : next(finite_cy_iter))

oder sich wiederholend

In [45]: loop_cy_iter = cyl()

In [46]: dd_loop = defaultdict(lambda : next(loop_cy_iter))

Dies kann hilfreich sein, wenn komplexe Daten geplottet werden, die sowohl eine Klassifizierung als auch ein Label haben

finite_cy_iter = iter(cyl)
styles = defaultdict(lambda : next(finite_cy_iter))
for group, label, data in DataSet:
    ax.plot(data, label=label, **styles[group])

was dazu führt, dass jedes data mit demselben group mit demselben Stil geplottet wird.

Ausnahmen

Ein ValueError wird ausgelöst, wenn Cycler mit ungleicher Länge zusammen addiert werden

In [47]: cycler(c=['r', 'g', 'b']) + cycler(ls=['-', '--'])
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[47], line 1
----> 1 cycler(c=['r', 'g', 'b']) + cycler(ls=['-', '--'])

File ~/code/cycler/venv/lib64/python3.12/site-packages/cycler/__init__.py:283, in Cycler.__add__(self, other)
    275 """
    276 Pair-wise combine two equal length cyclers (zip).
    277 
   (...)
    280 other : Cycler
    281 """
    282 if len(self) != len(other):
--> 283     raise ValueError(
    284         f"Can only add equal length cycles, not {len(self)} and {len(other)}"
    285     )
    286 return Cycler(
    287     cast(Cycler[Union[K, L], Union[V, U]], self),
    288     cast(Cycler[Union[K, L], Union[V, U]], other),
    289     zip
    290 )

ValueError: Can only add equal length cycles, not 3 and 2

oder wenn zwei Zyklen mit überlappenden Schlüsseln komponiert werden

In [48]: color_cycle = cycler(c=['r', 'g', 'b'])

In [49]: color_cycle + color_cycle
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[49], line 1
----> 1 color_cycle + color_cycle

File ~/code/cycler/venv/lib64/python3.12/site-packages/cycler/__init__.py:286, in Cycler.__add__(self, other)
    282 if len(self) != len(other):
    283     raise ValueError(
    284         f"Can only add equal length cycles, not {len(self)} and {len(other)}"
    285     )
--> 286 return Cycler(
    287     cast(Cycler[Union[K, L], Union[V, U]], self),
    288     cast(Cycler[Union[K, L], Union[V, U]], other),
    289     zip
    290 )

File ~/code/cycler/venv/lib64/python3.12/site-packages/cycler/__init__.py:179, in Cycler.__init__(self, left, right, op)
    176 else:
    177     self._right = None
--> 179 self._keys: set[K] = _process_keys(self._left, self._right)
    180 self._op: Any = op

File ~/code/cycler/venv/lib64/python3.12/site-packages/cycler/__init__.py:84, in _process_keys(left, right)
     82 r_key: set[K] = set(r_peek.keys())
     83 if l_key & r_key:
---> 84     raise ValueError("Can not compose overlapping cycles")
     85 return l_key | r_key

ValueError: Can not compose overlapping cycles

Motivation

Beim Plotten mehrerer Linien ist es üblich, über einen oder mehrere Künstlerstile iterieren zu können. Für einfache Fälle kann dies ohne große Schwierigkeiten geschehen

fig, ax = plt.subplots(tight_layout=True)
x = np.linspace(0, 2*np.pi, 1024)

for i, (lw, c) in enumerate(zip(range(4), ['r', 'g', 'b', 'k'])):
   ax.plot(x, np.sin(x - i * np.pi / 4),
           label=r'$\phi = {{{0}}} \pi / 4$'.format(i),
           lw=lw + 1,
           c=c)

ax.set_xlim([0, 2*np.pi])
ax.set_title(r'$y=\sin(\theta + \phi)$')
ax.set_ylabel(r'[arb]')
ax.set_xlabel(r'$\theta$ [rad]')

ax.legend(loc=0)

(Quellcode, png, hires.png, pdf)

_images/index-3.png

Wenn Sie jedoch etwas Komplizierteres tun möchten

fig, ax = plt.subplots(tight_layout=True)
x = np.linspace(0, 2*np.pi, 1024)

for i, (lw, c) in enumerate(zip(range(4), ['r', 'g', 'b', 'k'])):
   if i % 2:
       ls = '-'
   else:
       ls = '--'
   ax.plot(x, np.sin(x - i * np.pi / 4),
           label=r'$\phi = {{{0}}} \pi / 4$'.format(i),
           lw=lw + 1,
           c=c,
           ls=ls)

ax.set_xlim([0, 2*np.pi])
ax.set_title(r'$y=\sin(\theta + \phi)$')
ax.set_ylabel(r'[arb]')
ax.set_xlabel(r'$\theta$ [rad]')

ax.legend(loc=0)

(Quellcode, png, hires.png, pdf)

_images/index-4.png

kann die Plot-Logik schnell sehr kompliziert werden. Um dies zu beheben und eine einfache Iteration über beliebige kwargs zu ermöglichen, wurde die Klasse Cycler, ein komponierbarer Schlüsselwortargument-Iterator, entwickelt.