polyphys 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. polyphys/__init__.py +7 -0
  2. polyphys/__version__.py +22 -0
  3. polyphys/analyze/__init__.py +51 -0
  4. polyphys/analyze/clusters.py +1738 -0
  5. polyphys/analyze/correlations.py +995 -0
  6. polyphys/analyze/density.py +1138 -0
  7. polyphys/analyze/measurer.py +1338 -0
  8. polyphys/analyze/pipeline.py +1868 -0
  9. polyphys/analyze/stats.py +810 -0
  10. polyphys/analyze/sumrule.py +1159 -0
  11. polyphys/manage/__init__.py +11 -0
  12. polyphys/manage/organizer.py +2188 -0
  13. polyphys/manage/parser.py +1264 -0
  14. polyphys/manage/types.py +171 -0
  15. polyphys/manage/utils.py +777 -0
  16. polyphys/probe/__init__.py +25 -0
  17. polyphys/probe/binders.py +711 -0
  18. polyphys/probe/chain_size.py +583 -0
  19. polyphys/probe/core.py +437 -0
  20. polyphys/probe/histograms.py +863 -0
  21. polyphys/probe/pairs.py +555 -0
  22. polyphys/simulate/__init__.py +48 -0
  23. polyphys/simulate/build.py +1231 -0
  24. polyphys/simulate/core.py +237 -0
  25. polyphys/simulate/data.py +977 -0
  26. polyphys/simulate/dump.py +752 -0
  27. polyphys/simulate/log.py +788 -0
  28. polyphys/spec.py +1359 -0
  29. polyphys/tests/analyze/test_clusters.py +1058 -0
  30. polyphys/tests/analyze/test_correlations.py +708 -0
  31. polyphys/tests/analyze/test_density.py +813 -0
  32. polyphys/tests/analyze/test_measurer.py +785 -0
  33. polyphys/tests/analyze/test_pipeline.py +1881 -0
  34. polyphys/tests/analyze/test_stats.py +732 -0
  35. polyphys/tests/analyze/test_sumrule.py +917 -0
  36. polyphys/tests/conftest.py +753 -0
  37. polyphys/tests/data/lammps_build/make_starts.py +70 -0
  38. polyphys/tests/manage/test_organizer.py +1495 -0
  39. polyphys/tests/manage/test_parser.py +1222 -0
  40. polyphys/tests/manage/test_utils.py +498 -0
  41. polyphys/tests/probe/conftest.py +407 -0
  42. polyphys/tests/probe/test_binders.py +686 -0
  43. polyphys/tests/probe/test_chain_size.py +557 -0
  44. polyphys/tests/probe/test_core.py +391 -0
  45. polyphys/tests/probe/test_histograms.py +779 -0
  46. polyphys/tests/probe/test_pairs.py +475 -0
  47. polyphys/tests/simulate/test_build.py +1042 -0
  48. polyphys/tests/simulate/test_core.py +106 -0
  49. polyphys/tests/simulate/test_data.py +858 -0
  50. polyphys/tests/simulate/test_dump.py +487 -0
  51. polyphys/tests/simulate/test_log.py +667 -0
  52. polyphys/tests/strategies.py +120 -0
  53. polyphys/tests/synthetic.py +410 -0
  54. polyphys/tests/test_conftest.py +414 -0
  55. polyphys/tests/test_docs.py +82 -0
  56. polyphys/tests/test_packaging.py +97 -0
  57. polyphys/tests/test_spec.py +772 -0
  58. polyphys/tests/viz/conftest.py +32 -0
  59. polyphys/tests/viz/test_colors.py +113 -0
  60. polyphys/tests/viz/test_helpers.py +292 -0
  61. polyphys/viz/__init__.py +15 -0
  62. polyphys/viz/colors.py +204 -0
  63. polyphys/viz/helpers.py +549 -0
  64. polyphys-1.0.0.dist-info/METADATA +339 -0
  65. polyphys-1.0.0.dist-info/RECORD +69 -0
  66. polyphys-1.0.0.dist-info/WHEEL +5 -0
  67. polyphys-1.0.0.dist-info/licenses/AUTHORS.rst +13 -0
  68. polyphys-1.0.0.dist-info/licenses/LICENSE +22 -0
  69. polyphys-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,549 @@
1
+ """
2
+ ==========================================================
3
+ :mod:`polyphys.viz.helpers`
4
+ ==========================================================
5
+
6
+ The :mod:`polyphys.viz.helpers` module holds the matplotlib helpers the
7
+ studies' multi-panel figures use: a rule for placing an axis's ticks, proxy
8
+ legend handles, a legend moved into chosen panels, and a band shaded under
9
+ each line. It needs matplotlib, which the ``viz`` extra installs, and raises
10
+ :exc:`ImportError` with that hint when it is missing.
11
+
12
+ Ticks
13
+ =====
14
+
15
+ :func:`set_xticks` and :func:`set_yticks` place the major ticks of one axis
16
+ by one rule. Given `lower`, `upper`, and `step`:
17
+
18
+ - the ticks are :math:`t_k = \\mathrm{lower} + k \\, \\mathrm{step}` for
19
+ :math:`k = 0, 1, \\dots, \\lfloor (\\mathrm{upper} - \\mathrm{lower}) /
20
+ \\mathrm{step} \\rfloor`, so the last tick is `upper` when `step` divides
21
+ the span;
22
+ - the *tolerance* is ``step / 10`` rounded down to its first non-zero
23
+ digit, by :func:`polyphys.manage.utils.round_down_first_non_zero`; a tick
24
+ closer to zero than the tolerance is written as exactly ``0.0``, so
25
+ ``-5.6e-17`` never appears as a label;
26
+ - the axis limits are ``(lower - tolerance, upper + tolerance)``, a frame
27
+ that keeps the first and last ticks off the axes' edges;
28
+ - the labels are the ticks rounded to `decimals` places, or hidden.
29
+
30
+ The ticks are not rounded to multiples of a power of ten; the caller
31
+ chooses `lower`, `upper`, and `step` with that in mind.
32
+
33
+ Legends
34
+ =======
35
+
36
+ - :func:`color_handles` and :func:`line_handles` make *proxy* handles,
37
+ artists that draw nothing on the axes and exist only to sit in a legend,
38
+ so a figure can carry one legend for its colors and another for its line
39
+ styles or markers, whatever the plotting call produced.
40
+ - :func:`move_legend_to_axes` takes an existing legend, such as the figure
41
+ legend a facet grid draws outside its panels, and re-creates it inside
42
+ chosen panels.
43
+ - :func:`band_under_lines` shades a band, such as a confidence interval,
44
+ under each line already drawn on an axes, in that line's color.
45
+
46
+ Examples
47
+ ========
48
+
49
+ >>> import matplotlib
50
+ >>> matplotlib.use("Agg")
51
+ >>> import matplotlib.pyplot as plt
52
+ >>> from polyphys.viz.helpers import color_handles, set_xticks
53
+ >>> fig, ax = plt.subplots()
54
+ >>> set_xticks(ax, 0.0, 1.0, 0.25)
55
+ array([0. , 0.25, 0.5 , 0.75, 1. ])
56
+ >>> [float(limit) for limit in ax.get_xlim()]
57
+ [-0.02, 1.02]
58
+ >>> handles = color_handles(["bug", "crowder"], ["#4477AA", "#EE6677"])
59
+ >>> [text.get_text() for text in ax.legend(handles=handles).get_texts()]
60
+ ['bug', 'crowder']
61
+ >>> plt.close(fig)
62
+ """
63
+ import numbers
64
+ from typing import Any, Literal, Optional, Sequence, Union
65
+
66
+ import numpy as np
67
+
68
+ from ..manage.utils import round_down_first_non_zero
69
+
70
+ try:
71
+ import matplotlib # noqa: F401
72
+ except ImportError as error: # pragma: no cover - depends on the install
73
+ raise ImportError(
74
+ "polyphys.viz.helpers needs matplotlib; install it with "
75
+ "python -m pip install 'polyphys[viz]'.") from error
76
+
77
+ from matplotlib.axes import Axes
78
+ from matplotlib.collections import PolyCollection
79
+ from matplotlib.legend import Legend
80
+ from matplotlib.lines import Line2D
81
+ from matplotlib.patches import Patch
82
+
83
+ __all__ = ["band_under_lines", "color_handles", "line_handles",
84
+ "move_legend_to_axes", "set_xticks", "set_yticks"]
85
+
86
+
87
+ def _ticks(lower: float, upper: float, step: float) -> tuple[Any, float]:
88
+ """Return the tick positions and the tolerance of the ticks rule."""
89
+ if not lower < upper:
90
+ raise ValueError(
91
+ f"lower must be below upper, got lower={lower} and "
92
+ f"upper={upper}.")
93
+ span = upper - lower
94
+ if step <= 0 or step >= span:
95
+ raise ValueError(
96
+ f"step must be positive and smaller than the span "
97
+ f"upper - lower = {span}, got step={step}.")
98
+ tolerance = round_down_first_non_zero(step / 10)
99
+ # The 1e-9 absorbs the float error of a span that step divides exactly.
100
+ count = int(np.floor(span / step + 1e-9)) + 1
101
+ ticks = lower + step * np.arange(count)
102
+ ticks = np.where(np.abs(ticks) < tolerance, 0.0, ticks)
103
+ return ticks, tolerance
104
+
105
+
106
+ def _set_ticks(
107
+ ax: Axes,
108
+ which: Literal["x", "y"],
109
+ lower: float,
110
+ upper: float,
111
+ step: float,
112
+ labels: bool,
113
+ decimals: int,
114
+ text_kwargs: dict[str, Any],
115
+ ) -> Any:
116
+ """Apply the ticks rule to the x or y axis of `ax`."""
117
+ ticks, tolerance = _ticks(lower, upper, step)
118
+ texts = ([str(v) for v in np.around(ticks, decimals)] if labels
119
+ else [""] * len(ticks))
120
+ if which == "x":
121
+ ax.set_xlim(lower - tolerance, upper + tolerance)
122
+ ax.set_xticks(ticks)
123
+ ax.set_xticklabels(texts, **text_kwargs)
124
+ else:
125
+ ax.set_ylim(lower - tolerance, upper + tolerance)
126
+ ax.set_yticks(ticks)
127
+ ax.set_yticklabels(texts, **text_kwargs)
128
+ return ticks
129
+
130
+
131
+ def set_xticks(
132
+ ax: Axes,
133
+ lower: float,
134
+ upper: float,
135
+ step: float,
136
+ *,
137
+ labels: bool = True,
138
+ decimals: int = 3,
139
+ **text_kwargs: Any,
140
+ ) -> Any:
141
+ """
142
+ Place the major x ticks of `ax` every `step` from `lower` to `upper`.
143
+
144
+ The ticks, the tolerance that frames them, and the labels follow the
145
+ rule in the module docstring. Time and space are :math:`O(n)` in the
146
+ number of ticks.
147
+
148
+ Parameters
149
+ ----------
150
+ ax : matplotlib.axes.Axes
151
+ The axes whose x axis is set.
152
+ lower, upper : float
153
+ The first tick and the largest tick value, in data units;
154
+ ``lower < upper``.
155
+ step : float
156
+ The distance between ticks, positive and smaller than
157
+ ``upper - lower``.
158
+ labels : bool, default True
159
+ Whether the ticks are labeled; ``False`` hides every label.
160
+ decimals : int, default 3
161
+ The number of decimals the labels are rounded to.
162
+ **text_kwargs
163
+ Passed to :meth:`matplotlib.axes.Axes.set_xticklabels`, such as
164
+ ``fontsize``.
165
+
166
+ Returns
167
+ -------
168
+ numpy.ndarray
169
+ The tick positions, shape ``(n_ticks,)``.
170
+
171
+ Raises
172
+ ------
173
+ ValueError
174
+ If ``lower >= upper``, or `step` is not positive and smaller than
175
+ ``upper - lower``.
176
+
177
+ Examples
178
+ --------
179
+ >>> import matplotlib
180
+ >>> matplotlib.use("Agg")
181
+ >>> import matplotlib.pyplot as plt
182
+ >>> fig, ax = plt.subplots()
183
+ >>> set_xticks(ax, -0.2, 0.2, 0.1, decimals=1)
184
+ array([-0.2, -0.1, 0. , 0.1, 0.2])
185
+ >>> [label.get_text() for label in ax.get_xticklabels()]
186
+ ['-0.2', '-0.1', '0.0', '0.1', '0.2']
187
+ >>> plt.close(fig)
188
+ """
189
+ return _set_ticks(ax, "x", lower, upper, step, labels, decimals,
190
+ text_kwargs)
191
+
192
+
193
+ def set_yticks(
194
+ ax: Axes,
195
+ lower: float,
196
+ upper: float,
197
+ step: float,
198
+ *,
199
+ labels: bool = True,
200
+ decimals: int = 3,
201
+ **text_kwargs: Any,
202
+ ) -> Any:
203
+ """
204
+ Place the major y ticks of `ax` every `step` from `lower` to `upper`.
205
+
206
+ The y-axis twin of :func:`set_xticks`, with the same rule, parameters,
207
+ return value, and errors; `text_kwargs` go to
208
+ :meth:`matplotlib.axes.Axes.set_yticklabels`.
209
+
210
+ Parameters
211
+ ----------
212
+ ax : matplotlib.axes.Axes
213
+ The axes whose y axis is set.
214
+ lower, upper : float
215
+ The first tick and the largest tick value; ``lower < upper``.
216
+ step : float
217
+ The distance between ticks, positive and smaller than
218
+ ``upper - lower``.
219
+ labels : bool, default True
220
+ Whether the ticks are labeled.
221
+ decimals : int, default 3
222
+ The number of decimals the labels are rounded to.
223
+ **text_kwargs
224
+ Passed to :meth:`matplotlib.axes.Axes.set_yticklabels`.
225
+
226
+ Returns
227
+ -------
228
+ numpy.ndarray
229
+ The tick positions, shape ``(n_ticks,)``.
230
+
231
+ Examples
232
+ --------
233
+ >>> import matplotlib
234
+ >>> matplotlib.use("Agg")
235
+ >>> import matplotlib.pyplot as plt
236
+ >>> fig, ax = plt.subplots()
237
+ >>> set_yticks(ax, 0.0, 1.0, 0.5, labels=False)
238
+ array([0. , 0.5, 1. ])
239
+ >>> [float(limit) for limit in ax.get_ylim()]
240
+ [-0.05, 1.05]
241
+ >>> plt.close(fig)
242
+ """
243
+ return _set_ticks(ax, "y", lower, upper, step, labels, decimals,
244
+ text_kwargs)
245
+
246
+
247
+ def _per_label(
248
+ name: str,
249
+ values: Union[None, str, float, Sequence[Any]],
250
+ n_labels: int,
251
+ ) -> list[Any]:
252
+ """Return one value per label: one value repeated, or a matching list.
253
+
254
+ One value is None, a string, a real number (NumPy scalars included), a
255
+ tuple of real numbers such as an RGB or RGBA color, or a dash tuple
256
+ ``(offset, (on, off, ...))``. Anything else is a sequence with one entry
257
+ per label.
258
+ """
259
+ if _is_one_value(values):
260
+ return [values] * n_labels
261
+ values = list(values)
262
+ if len(values) != n_labels:
263
+ raise ValueError(
264
+ f"{name} has {len(values)} entries for {n_labels} labels.")
265
+ return values
266
+
267
+
268
+ def _is_one_value(value: Any) -> bool:
269
+ """Return True if `value` is one style, not one per label."""
270
+ if value is None or isinstance(value, (str, numbers.Real)):
271
+ return True
272
+ if not isinstance(value, tuple):
273
+ return False
274
+ if all(isinstance(item, numbers.Real) for item in value):
275
+ return True
276
+ return (len(value) == 2 and isinstance(value[0], numbers.Real)
277
+ and isinstance(value[1], (tuple, list))
278
+ and all(isinstance(item, numbers.Real) for item in value[1]))
279
+
280
+
281
+ def color_handles(
282
+ labels: Sequence[str],
283
+ colors: Any,
284
+ **patch_kwargs: Any,
285
+ ) -> list[Patch]:
286
+ """
287
+ Make one filled :class:`~matplotlib.patches.Patch` per label.
288
+
289
+ The patches are proxy handles for a legend keyed by color. Time and
290
+ space are :math:`O(n)` in the number of labels.
291
+
292
+ Parameters
293
+ ----------
294
+ labels : sequence of str
295
+ The legend entries.
296
+ colors : matplotlib color or sequence of matplotlib colors
297
+ One color for every label, a name or a tuple of numbers such as the
298
+ RGB triple ``(0.1, 0.2, 0.3)``; or a list, or another sequence, with
299
+ one color per label, in the same order.
300
+ **patch_kwargs
301
+ Passed to every :class:`~matplotlib.patches.Patch`, such as
302
+ ``alpha``.
303
+
304
+ Returns
305
+ -------
306
+ list of matplotlib.patches.Patch
307
+ The handles, one per label, for the ``handles`` argument of
308
+ :meth:`matplotlib.axes.Axes.legend`.
309
+
310
+ Raises
311
+ ------
312
+ ValueError
313
+ If `colors` is a sequence of colors whose length differs from that
314
+ of `labels`.
315
+
316
+ Examples
317
+ --------
318
+ >>> patches = color_handles(["low", "high"], ["#4477AA", "#EE6677"],
319
+ ... alpha=0.5)
320
+ >>> [patch.get_label() for patch in patches]
321
+ ['low', 'high']
322
+ >>> patches[0].get_alpha()
323
+ 0.5
324
+
325
+ One RGB tuple colors every label:
326
+
327
+ >>> patches = color_handles(["a", "b"], (0.0, 0.5, 1.0))
328
+ >>> [patch.get_facecolor()[:3] for patch in patches]
329
+ [(0.0, 0.5, 1.0), (0.0, 0.5, 1.0)]
330
+ """
331
+ colors = _per_label("colors", colors, len(labels))
332
+ return [Patch(label=label, color=color, **patch_kwargs)
333
+ for label, color in zip(labels, colors)]
334
+
335
+
336
+ def line_handles(
337
+ labels: Sequence[str],
338
+ *,
339
+ linestyles: Union[None, str, Sequence[Any]] = None,
340
+ linewidths: Union[None, float, Sequence[float]] = None,
341
+ markers: Union[None, str, Sequence[Any]] = None,
342
+ color: Any = "black",
343
+ **line_kwargs: Any,
344
+ ) -> list[Line2D]:
345
+ """
346
+ Make one empty :class:`~matplotlib.lines.Line2D` per label.
347
+
348
+ The lines are proxy handles for a legend keyed by line style, width,
349
+ or marker. Each of `linestyles`, `linewidths`, and `markers` is either
350
+ one value, used for every label, a sequence with one entry per label,
351
+ or ``None``, which leaves that attribute at matplotlib's default. One
352
+ value is a string, a number, or a dash tuple such as ``(0, (5, 2))``;
353
+ a list, or a tuple of other items, is one entry per label. Time
354
+ and space are :math:`O(n)` in the number of labels.
355
+
356
+ Parameters
357
+ ----------
358
+ labels : sequence of str
359
+ The legend entries.
360
+ linestyles : str or sequence, optional
361
+ The line style of each handle, such as ``'-'`` or ``'--'``.
362
+ linewidths : float or sequence of float, optional
363
+ The line width of each handle, in points.
364
+ markers : str or sequence, optional
365
+ The marker of each handle, such as ``'o'`` or ``'^'``.
366
+ color : matplotlib color, default 'black'
367
+ The color of every handle.
368
+ **line_kwargs
369
+ Passed to every :class:`~matplotlib.lines.Line2D`, such as
370
+ ``markersize``.
371
+
372
+ Returns
373
+ -------
374
+ list of matplotlib.lines.Line2D
375
+ The handles, one per label.
376
+
377
+ Raises
378
+ ------
379
+ ValueError
380
+ If a sequence among `linestyles`, `linewidths`, and `markers` does
381
+ not have one entry per label.
382
+
383
+ Examples
384
+ --------
385
+ >>> lines = line_handles(["ring", "linear"], linestyles=["-", "--"],
386
+ ... markers="o", linewidths=2)
387
+ >>> [(line.get_label(), line.get_linestyle(), line.get_marker())
388
+ ... for line in lines]
389
+ [('ring', '-', 'o'), ('linear', '--', 'o')]
390
+ """
391
+ n_labels = len(labels)
392
+ styles = _per_label("linestyles", linestyles, n_labels)
393
+ widths = _per_label("linewidths", linewidths, n_labels)
394
+ marks = _per_label("markers", markers, n_labels)
395
+ handles = []
396
+ for label, style, width, mark in zip(labels, styles, widths, marks):
397
+ kwargs = {"linestyle": style, "linewidth": width, "marker": mark}
398
+ kwargs = {k: v for k, v in kwargs.items() if v is not None}
399
+ handles.append(
400
+ Line2D([], [], label=label, color=color, **kwargs,
401
+ **line_kwargs))
402
+ return handles
403
+
404
+
405
+ def move_legend_to_axes(
406
+ legend: Legend,
407
+ axes: Sequence[Axes],
408
+ locs: Sequence[Any],
409
+ *,
410
+ title: Optional[str] = None,
411
+ **legend_kwargs: Any,
412
+ ) -> list[Legend]:
413
+ """
414
+ Remove `legend` and re-create it inside each of `axes`.
415
+
416
+ The new legends carry the handles, labels, and title of `legend`; each
417
+ replaces the legend its axes had. This moves, for instance, the legend
418
+ a seaborn facet grid draws beside its panels into one or more panels.
419
+ Time and space are :math:`O(n_\\mathrm{axes} \\, n_\\mathrm{entries})`.
420
+
421
+ Parameters
422
+ ----------
423
+ legend : matplotlib.legend.Legend
424
+ The legend to move; it is removed from its figure or axes.
425
+ axes : sequence of matplotlib.axes.Axes
426
+ The axes that receive a copy each.
427
+ locs : sequence
428
+ One legend location per axes, as the ``loc`` argument of
429
+ :meth:`matplotlib.axes.Axes.legend` takes it.
430
+ title : str, optional
431
+ A new title; by default the title of `legend` is kept.
432
+ **legend_kwargs
433
+ Passed to every :meth:`matplotlib.axes.Axes.legend` call, such as
434
+ ``frameon``.
435
+
436
+ Returns
437
+ -------
438
+ list of matplotlib.legend.Legend
439
+ The new legends, in the order of `axes`.
440
+
441
+ Raises
442
+ ------
443
+ ValueError
444
+ If `axes` and `locs` differ in length.
445
+
446
+ Examples
447
+ --------
448
+ >>> import matplotlib
449
+ >>> matplotlib.use("Agg")
450
+ >>> import matplotlib.pyplot as plt
451
+ >>> fig, (left, right) = plt.subplots(ncols=2)
452
+ >>> _ = left.plot([0, 1], [0, 1], label="rise")
453
+ >>> outer = fig.legend(title="curves")
454
+ >>> inner = move_legend_to_axes(outer, [right], ["upper left"])
455
+ >>> fig.legends
456
+ []
457
+ >>> [text.get_text() for text in right.get_legend().get_texts()]
458
+ ['rise']
459
+ >>> right.get_legend().get_title().get_text()
460
+ 'curves'
461
+ >>> plt.close(fig)
462
+ """
463
+ if len(axes) != len(locs):
464
+ raise ValueError(
465
+ f"locs has {len(locs)} entries for {len(axes)} axes.")
466
+ entries = [(handle, text.get_text())
467
+ for handle, text in zip(legend.legend_handles,
468
+ legend.get_texts())
469
+ if handle is not None]
470
+ handles = [handle for handle, _ in entries]
471
+ labels = [label for _, label in entries]
472
+ new_title = legend.get_title().get_text() if title is None else title
473
+ legend.remove()
474
+ return [ax.legend(handles, labels, loc=loc, title=new_title,
475
+ **legend_kwargs)
476
+ for ax, loc in zip(axes, locs)]
477
+
478
+
479
+ def band_under_lines(
480
+ ax: Axes,
481
+ bands: Sequence[tuple[Any, Any]],
482
+ *,
483
+ alpha: float = 0.25,
484
+ **fill_kwargs: Any,
485
+ ) -> list[PolyCollection]:
486
+ """
487
+ Shade one band under each line of `ax`, in that line's color.
488
+
489
+ ``bands[i]`` is the pair ``(lower, upper)`` of the band under
490
+ ``ax.lines[i]``, each an array over that line's x data; the band is
491
+ drawn with :meth:`matplotlib.axes.Axes.fill_between`. Lines beyond
492
+ ``len(bands)`` get no band. Time and space are :math:`O(n_\\mathrm{lines}
493
+ \\, n_\\mathrm{points})`.
494
+
495
+ Parameters
496
+ ----------
497
+ ax : matplotlib.axes.Axes
498
+ The axes whose lines are shaded, in drawing order.
499
+ bands : sequence of pairs of array_like
500
+ The lower and upper edge of each band, shape ``(n_points,)`` each,
501
+ matching the x data of the line.
502
+ alpha : float, default 0.25
503
+ The opacity of every band.
504
+ **fill_kwargs
505
+ Passed to every :meth:`matplotlib.axes.Axes.fill_between` call,
506
+ such as ``linewidth``.
507
+
508
+ Returns
509
+ -------
510
+ list of matplotlib.collections.PolyCollection
511
+ The bands, in the order of the lines.
512
+
513
+ Raises
514
+ ------
515
+ ValueError
516
+ If there are more bands than lines, or a band edge's length differs
517
+ from its line's number of points.
518
+
519
+ Examples
520
+ --------
521
+ >>> import matplotlib
522
+ >>> matplotlib.use("Agg")
523
+ >>> import matplotlib.pyplot as plt
524
+ >>> fig, ax = plt.subplots()
525
+ >>> _ = ax.plot([0, 1, 2], [1, 1, 1], color="#4477AA")
526
+ >>> bands = band_under_lines(ax, [([0.5, 0.5, 0.5], [1.5, 1.5, 1.5])])
527
+ >>> len(bands)
528
+ 1
529
+ >>> bands[0].get_facecolor()[0].round(2).tolist()
530
+ [0.27, 0.47, 0.67, 0.25]
531
+ >>> plt.close(fig)
532
+ """
533
+ lines = list(ax.lines)
534
+ if len(bands) > len(lines):
535
+ raise ValueError(
536
+ f"{len(bands)} bands were given for {len(lines)} lines.")
537
+ collections: list[PolyCollection] = []
538
+ for line, (lower, upper) in zip(lines, bands):
539
+ x = np.asarray(line.get_xdata(), dtype=float)
540
+ lower = np.asarray(lower, dtype=float)
541
+ upper = np.asarray(upper, dtype=float)
542
+ if lower.shape != x.shape or upper.shape != x.shape:
543
+ raise ValueError(
544
+ f"a band has edges of shape {lower.shape} and "
545
+ f"{upper.shape} for a line of {x.shape[0]} points.")
546
+ collections.append(
547
+ ax.fill_between(x, lower, upper, color=line.get_color(),
548
+ alpha=alpha, **fill_kwargs))
549
+ return collections