Composable cycles

Version:

0.12

Datum:

18. Jan 2024

Docs

https://matplotlib.de/cycler

PyPI

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

GitHub

https://github.com/matplotlib/cycler

cycler API

cycler()

Erstellen Sie 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)

Verketten Sie Cyclers, als ob sie mit itertools.chain verknüpft wären.

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 ein Schlüsselwortargument/Stil/Schlüsselwort an eine Reihe von Werten zu binden. 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 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 verschlagwortet 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 (ähnlich einem 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 einzige for-Schleife ersetzt werden. Die Stärke von Cycler-Objekten liegt darin, dass sie kombiniert werden können, um einfach komplexe Multi-Key-Zyklen zu erstellen.

Addition

Gleich lange Cyclers mit 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-Cyclers sind.

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

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

und die Iteration über das Ergebnis ist das Zip 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 aufnehmen und sie automatisch durch Addition zu einem einzigen Cycler kombinieren

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 Cyclers 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 (dasselbe wie 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

Cyclers 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 verwendet werden, um einen neuen Cycler mit den aktualisierten Werten zu erstellen

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, ein gegebenes Label mit einem Stil über eine Wörterbuchsuche zu verknüpfen und diese Zuordnung dynamisch zu generieren. Dies kann einfach 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 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])

wodurch jedes data mit demselben group mit demselben Stil geplottet wird.

Ausnahmen

Ein ValueError wird ausgelöst, wenn ungleiche Cyclers 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 kombiniert 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 wollen. Für einfache Fälle kann dies ohne zu große Probleme erfolgen

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 Komplexeres 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 aufwendig werden. Um dies zu beheben und das einfache Iterieren über beliebige kwargs zu ermöglichen, wurde die Klasse Cycler, ein zusammensetzbarer Keyword-Argument-Iterator, entwickelt.