Skip to content

curvey.curves¤

curves ¤

Sequences of curves

Curves ¤

A container of Curves

The constructor accepts an iterator of Curves:

from curvey import Curve, Curves

curves = Curves(Curve.circle(n=20, r=r) for r in (1, 2, 3))

Curves can be iterated over:

for c in curves:
    ...

Curves can be indexed with:

  • an int: curves[i] returns the Curve at the iith position

  • a slice or numpy index-like array: curves[::2] returns a new Curves containing every second curve

  • a string: curves['param'] returns an array of the curve metadata or Curve attribute associated with each Curve in the Curves

  • a function: curves[fn] is equivalent to array([fn(c) for c in curves])

Use Curves.subplots or Curves.superimposed to plot every curve in the Curves at once.

Use Curves.plot to plot Curve metadata values against eachother.

Parameters:

Name Type Description Default
curves Iterable[Curve] | None

A iterable of Curves. Defaults to an empty sequence if not supplied.

None
Source code in src\curvey\curves.py
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
class Curves:
    """A container of `Curve`s

    The constructor accepts an iterator of `Curve`s:
    ```python
    from curvey import Curve, Curves

    curves = Curves(Curve.circle(n=20, r=r) for r in (1, 2, 3))
    ```

    `Curves` can be iterated over:
    ```python
    for c in curves:
        ...
    ```

    `Curves` can be indexed with:

    - an int: `curves[i]` returns the `Curve` at the `i`ith position

    - a slice or numpy index-like array: `curves[::2]` returns a new `Curves` containing every
        second curve

    - a string: `curves['param']` returns an array of the curve metadata or `Curve` attribute
        associated with each `Curve` in the `Curves`

    - a function: `curves[fn]` is equivalent to `array([fn(c) for c in curves])`

    Use `Curves.subplots` or `Curves.superimposed` to plot every curve in the `Curves` at once.

    Use `Curves.plot` to plot `Curve` metadata values against eachother.

    Parameters
    ----------
    curves
        A iterable of `Curves`. Defaults to an empty sequence if not supplied.
    """

    def __init__(self, curves: Iterable[Curve] | None = None):
        self.curves: list[Curve] = []
        """The `Curve`s contained in this `Curves`"""

        if curves is not None:
            self.curves.extend(curves)

    @property
    def n(self) -> int:
        """Number of curves in the sequence"""
        return len(self.curves)

    def __len__(self) -> int:
        """Number of curves in the sequence"""
        return len(self.curves)

    def __add__(self, other: Curves | list[Curve]) -> Curves:
        """Concatenate two `Curves` to form a new sequence"""
        if isinstance(other, Curves):
            return Curves(self.curves + other.curves)

        # Just hope this raises a reasonable error if it fails
        return Curves(self.curves + other)

    def append(self, curve: Curve):
        """Add a curve to the end of the sequence"""
        self.curves.append(curve)

    def keys(self, mode: Literal["intersection", "union"] = "union") -> set[str]:
        """Unique curve metadata parameter names

        Parameters
        ----------
        mode
            If 'union', return keys that are present on any `Curve` in the `Curves`.
            If 'intersection', return only keys that are present on all `Curve`s in the `Curves`.
        """
        if self.n == 0:
            return set()

        keys = (set(c.data.keys()) for c in self)
        if mode == "intersection":
            return set.intersection(*keys)

        if mode == "union":
            return set.union(*keys)

        raise ValueError(mode)

    def __repr__(self) -> str:
        if keys := self.keys():
            data = ", ".join(k for k in sorted(keys))
            # Gross triple nested curlies here, so it formats like `Curves(n=3, data={foo, bar})`
            return f"Curves(n={self.n}, data={{{data}}})"

        return f"Curves(n={self.n})"

    def __iter__(self) -> Iterator[Curve]:
        """Iterate over curves in the sequence

        The index of the curve is stored in the 'idx' metadata parameter.
        Use `Curves.iter_curves` to supply a custom name for index, if necessary.
        """
        return self.iter_curves()

    def iter_curves(self, idx: str = "idx") -> Iterator[Curve]:
        """Iterate over curves in the sequence

        The index of the curve is stored in the `idx` metadata parameter. This might be useful for
        tracking the original index in a subset.

        ```python
        from curvey import Curves

        orig = Curves(Curve.circle(3) for _ in range(6))
        every_other = Curves(orig.iter_curves('orig_idx'))[::2]
        every_other['orig_idx']
        ```
        """
        for i, c in enumerate(self.curves):
            yield c.with_data(**{idx: i})

    def get_named_data(self, data: BareMetadata | NamedMetadata, **kwargs) -> tuple[str, ndarray]:
        """Get curve metadata (name, values) pairs

        If `data` is just a name, return (name, values).
        If `data` is something that can reasonably interpreted as `values`,
        try to figure out a reasonable name for them.
        """
        if isinstance(data, str):
            return data, self.get_data(data, **kwargs)

        if isinstance(data, tuple) and len(data) == 2:
            name = data[0]
            if not isinstance(name, str):
                msg = f"Expected metadata name to be str, got {name}"
                raise TypeError(msg)
            return name, self.get_data(data[1], **kwargs)

        if callable(data):
            name = data.__name__ if hasattr(data, "__name__") else str(data)
            return name, self.get_data(data, **kwargs)

        name = type(data).__name__
        return name, self.get_data(data, **kwargs)

    def get_data(
        self,
        data: BareMetadata,
        default: Any = nan,
        allow_default: bool = True,
        allow_property: bool = True,
    ) -> ndarray:
        """Concatenate curve metadata into an array of length `n`

        Parameters
        ----------
        data
            One of:

            - Name of the property stored in curve metadata
            - Name of a `Curve` attribute, if `allow_property` is true
            - A function `Curve -> value`
            - An length `n` array or list of values

        allow_property
            If true, `data` may be the name of a Curve attribute, such as 'area' or 'length'

        allow_default
            If true, and the requested data is only available on a subset of curves, return
            `default` for those curves.

        default
            The default value if named parameter `data` is not present in a curve's metadata.
            If not supplied, all curves in the collection must have that metadata parameter,
            otherwise a `KeyError` is raised.

        """
        if isinstance(data, str):
            if data in self.keys("union"):
                if allow_default:
                    return array([c.data.get(data, default) for c in self])

                if data in self.keys("intersection"):
                    msg = f"Metadata '{data}' is only present on some curves"
                    raise KeyError(msg)

                return array([c[data] for c in self])

            if hasattr(Curve, data):
                if allow_property:
                    return array([getattr(c, data) for c in self])

                msg = (
                    f"Metadata '{data}' not found. "
                    "it's a Curve property but `allow_property` is False"
                )
                raise KeyError(msg)

            msg = f"Metadata '{data}' not found"
            raise KeyError(msg)

        if callable(data):
            return array([data(c) for c in self])

        if isinstance(data, Sequence):
            if (n := len(data)) != self.n:
                msg = f"Expected a sequence of length self.n = {self.n}, got {n}"
                raise ValueError(msg)
            return asarray(data)

        msg = f"Unrecognized data type {type(data)}"  # type: ignore [unreachable]
        raise TypeError(msg)

    @overload
    def __getitem__(self, idx: int) -> Curve: ...

    @overload
    def __getitem__(self, idx: slice) -> Curves: ...

    @overload
    def __getitem__(self, idx: str | collections.abc.Callable) -> ndarray: ...

    def __getitem__(
        self,
        idx: str | int | slice | ArrayLike | collections.abc.Callable,
    ) -> Curve | Curves | ndarray:
        """Convenience method to index the sequence

        `Curves[int]` returns the curve stored at that index.
        `Curves[str]` returns a `ndarray` of `n` metadata values.
        `Curves[fn]` for `fn: Callable[[Curve], Any]` returns a `ndarray` of the values
        of that function called on the `n` curves in the sequence.

        Otherwise, `Curves[idx]` returns a new `Curves` for that index,
        obeying list slicing and numpy smart indexing behavior. E.g. `sequence[::3]` returns
        a new curve sequence for every third curve in the original sequence.

        """
        if isinstance(idx, str):
            return self.get_data(idx)

        if callable(idx):
            return self.get_data(idx)

        if isinstance(idx, (int, np.integer)):
            # Recast to int here so indexing with np.int works as expected
            return self.curves[int(idx)]

        # noinspection PyUnresolvedReferences
        idx = arange(self.n)[idx]  # type: ignore [index]
        all_curves = self.curves
        curves = [all_curves[int(i)] for i in idx]
        return Curves(curves=curves)

    def plot(
        self,
        y: Metadata,
        x: Metadata | None = None,
        ax: Axes | None = None,
        label_axes: bool | None = None,
        label: str | bool | None = True,
        **kwargs,
    ):
        """Plot metadata values against each other.

        By default, the independent variable `y` is 'time', if it's present.

        Parameters
        ----------
        y
            The name of the parameter to plot on the y-axis.
            Can also be a function `Curve` -> float, or a `tuple(name, function)`,
            Or a `tuple(name, array)`

        x
            The name of the parameter to plot on the x-axis, or alternative type as described for
            `y`. If not supplied, defaults to `time` if it's present in the curve metadata,
            or index otherwise.

        label_axes
            If true, set x and y labels. Defaults to true if a new axes is created.

        label
            Name to label the plot with, for use in matplotlib legend. Defaults to the name
            of the `y` parameter.

        ax
            The matplotlib axes to plot in. Defaults to the current axes.

        **kwargs
            Remaining kwargs are passed to `matplotlib.pyplot.plot`

        """
        if ax is None:
            ax = plt.gca()
            if label_axes is None:
                label_axes = True

        yname, ydata = self.get_named_data(y)

        if x is None:
            keys = self.keys(mode="union")
            if "time" in keys:
                x = "time"
            elif "idx" in keys:
                x = "idx"
            else:
                x = ("idx", arange(self.n))

        xname, xdata = self.get_named_data(x)

        if label_axes:
            ax.set_xlabel(xname)
            ax.set_ylabel(yname)

        if label and isinstance(label, bool):
            label = yname

        ax.plot(xdata, ydata, label=label, **kwargs)

    def _subplots_builder(
        self,
        n_rows: int | None = 1,
        n_cols: int | None = None,
        share_xy=True,
        figsize: tuple[float, float] = (15, 5),
        idx: str = "idx",
        axs: Sequence[Axes] | None = None,
    ) -> _SubplotsBuilder:
        curves = Curves(list(self.iter_curves(idx=idx)))
        if axs is None:
            return _SubplotsBuilder.from_dims(
                curves=curves,
                nr=n_rows,
                nc=n_cols,
                sharex=share_xy,
                sharey=share_xy,
                figsize=figsize,
            )

        return _SubplotsBuilder.from_axs(curves=curves, axs=axs)

    def subplots(
        self,
        n_rows: int | None = 1,
        n_cols: int | None = None,
        axis: str | None = "scaled",
        show_axes=False,
        plot_fn: Callable[[Curve], None] | None = None,
        subtitle: str | Callable[[Curve], str] | None = None,
        share_xy=True,
        figsize: tuple[float, float] = (15, 5),
        idx: str = "idx",
        axs: Sequence[Axes] | None = None,
        hide_unused: bool = True,
    ):
        """Plot each curve in the sequence in a different subplot

        Parameters
        ----------
        figsize
            The size of the overall superfigure.

        n_rows
            Number of rows.

        n_cols
            Number of columns. By default, `n_cols` = ceil(self.n / n_rows)`. If `n_cols` is specified, and
            `n_rows` * `n_cols` < `self.n`, the curve sequence is automatically subsampled.

        axis
            Argument to `plt.axis`. By default this is 'equal' (i.e., make circles circular).

        show_axes
            Whether to show each subplot axes, i.e. border and x/y ticks, etc.

        plot_fn
            By default this just dispatches to `Curve.plot_edges(directed=False)`.

        subtitle
            A convenience argument to put a title over each subplot. If `subtitle` is a string,
            a title is constructed from the corresponding curve metadata. Otherwise, `subtitle`
            should be a function that accepts a curve and returns a string.

        share_xy
            Whether each subplot should share x/y limits.

        idx
            The index of the curve in this collection is stored in the curve metadata property
            with this name.

        axs
            Array of matplotlib axes to use for subplots. If supplied, `n_rows` and `n_cols`
            are ignored and determined by the shape of this array.

        hide_unused
            If n_axs > n_plots, hide the unused axes.

        Returns
        -------
        axes :
            `(n_rows, n_cols)` array of `matplotlib.axes.Axes` objects

        """
        if not plot_fn:

            def plot_fn(c: Curve):
                c.plot_edges(directed=False)

        if isinstance(subtitle, str):
            _key = subtitle

            def subtitle(c: Curve) -> str:
                return f"{_key} = {c[_key]}"

        b = self._subplots_builder(
            n_rows=n_rows,
            n_cols=n_cols,
            share_xy=share_xy,
            figsize=figsize,
            idx=idx,
            axs=axs,
        )
        if hide_unused and b.n_axs > b.n_plots:
            # Hide unused axes if there's e.g. 3x3 axes but only 8 curves to plot
            for i in range(b.n_plots, b.n_axs):
                b.get_ax(i).axis("off")

        for i in range(b.n_plots):
            b.axis_subplot(i=i, plot_fn=plot_fn, subtitle=subtitle, axis=axis, show_axes=show_axes)

        if b.fig is not None:  # If we created the figure
            b.fig.tight_layout()

        return b.axs

    def superimposed(
        self,
        ax: Axes | None = None,
        plot_fn: Callable[[Curve], Any] | None = None,
        color: Metadata | None = None,
        clim: tuple[float, float] | None = None,
        idx: str = "idx",
    ) -> list[Any]:
        """Plot every curve in the same axes

        Parameters
        ----------
        ax
            Matlplotlib axes to plot in. Default current axes.

        plot_fn
            Function `Curve -> matplotlib_object` to plot the curve.
            By default, this dispatches to `curve.plot`

        color
            The name of a curve metadata parameter to color by. If `plot_fn` is supplied,
            this is ignored.

        clim
            Range to scale color data to.

        idx
            The index of the curve in this collection is stored in this curve's metadata.

        Returns
        -------
        :
            List of objects returned by `plot_fn`.
        """

        ax = _get_ax(ax)

        if not plot_fn:
            if color is not None:
                cmap = plt.get_cmap("viridis")
                _cname, cdata = self.get_named_data(color)
                cnorm = plt.Normalize(*clim) if clim else plt.Normalize()
                cdata = cnorm(cdata)

                def plot_fn(c: Curve) -> Line2D:
                    return c.plot(color=cmap(cdata[c[idx]]), ax=ax)
            else:

                def plot_fn(c: Curve) -> Line2D:
                    return c.plot(ax=ax)

        out = []

        for curve in self.iter_curves(idx=idx):
            out.append(plot_fn(curve))

        return out

    def _animation_frames(self) -> Iterator[int]:
        i, step, n = 0, 1, self.n
        while True:
            yield i
            if (i, step) == (n - 1, 1):
                i, step = n - 2, -1
            elif (i, step) == (0, -1):
                i, step = 1, 1
            else:
                i += step

    def _animate(
        self,
        frames: Iterable[int] | Callable[[], int] | None = None,
        **kwargs,
    ):
        from matplotlib import animation

        kwargs.setdefault("save_count", 2 * self.n + 1)
        frames = frames or self._animation_frames()

        fig, ax = plt.subplots()
        line = self[0].plot(ax=ax)

        for c in self:
            ax.dataLim.update_from_data_xy(c.points)

        ax.axis("equal")
        ax.autoscale_view()

        def update(frame):
            curve = self[frame]
            x, y = curve.closed_points.T
            line.set_xdata(x)
            line.set_ydata(y)
            return line

        # noinspection PyTypeChecker
        return animation.FuncAnimation(
            fig=fig,
            func=update,
            frames=frames,  # type: ignore [arg-type]
            interval=30,
            **kwargs,
        )

curves: list[Curve] = [] instance-attribute ¤

The Curves contained in this Curves

n: int property ¤

Number of curves in the sequence

__add__(other: Curves | list[Curve]) -> Curves ¤

Concatenate two Curves to form a new sequence

Source code in src\curvey\curves.py
def __add__(self, other: Curves | list[Curve]) -> Curves:
    """Concatenate two `Curves` to form a new sequence"""
    if isinstance(other, Curves):
        return Curves(self.curves + other.curves)

    # Just hope this raises a reasonable error if it fails
    return Curves(self.curves + other)

__getitem__(idx: str | int | slice | ArrayLike | collections.abc.Callable) -> Curve | Curves | ndarray ¤

Convenience method to index the sequence

Curves[int] returns the curve stored at that index. Curves[str] returns a ndarray of n metadata values. Curves[fn] for fn: Callable[[Curve], Any] returns a ndarray of the values of that function called on the n curves in the sequence.

Otherwise, Curves[idx] returns a new Curves for that index, obeying list slicing and numpy smart indexing behavior. E.g. sequence[::3] returns a new curve sequence for every third curve in the original sequence.

Source code in src\curvey\curves.py
def __getitem__(
    self,
    idx: str | int | slice | ArrayLike | collections.abc.Callable,
) -> Curve | Curves | ndarray:
    """Convenience method to index the sequence

    `Curves[int]` returns the curve stored at that index.
    `Curves[str]` returns a `ndarray` of `n` metadata values.
    `Curves[fn]` for `fn: Callable[[Curve], Any]` returns a `ndarray` of the values
    of that function called on the `n` curves in the sequence.

    Otherwise, `Curves[idx]` returns a new `Curves` for that index,
    obeying list slicing and numpy smart indexing behavior. E.g. `sequence[::3]` returns
    a new curve sequence for every third curve in the original sequence.

    """
    if isinstance(idx, str):
        return self.get_data(idx)

    if callable(idx):
        return self.get_data(idx)

    if isinstance(idx, (int, np.integer)):
        # Recast to int here so indexing with np.int works as expected
        return self.curves[int(idx)]

    # noinspection PyUnresolvedReferences
    idx = arange(self.n)[idx]  # type: ignore [index]
    all_curves = self.curves
    curves = [all_curves[int(i)] for i in idx]
    return Curves(curves=curves)

__iter__() -> Iterator[Curve] ¤

Iterate over curves in the sequence

The index of the curve is stored in the 'idx' metadata parameter. Use Curves.iter_curves to supply a custom name for index, if necessary.

Source code in src\curvey\curves.py
def __iter__(self) -> Iterator[Curve]:
    """Iterate over curves in the sequence

    The index of the curve is stored in the 'idx' metadata parameter.
    Use `Curves.iter_curves` to supply a custom name for index, if necessary.
    """
    return self.iter_curves()

__len__() -> int ¤

Number of curves in the sequence

Source code in src\curvey\curves.py
def __len__(self) -> int:
    """Number of curves in the sequence"""
    return len(self.curves)

append(curve: Curve) ¤

Add a curve to the end of the sequence

Source code in src\curvey\curves.py
def append(self, curve: Curve):
    """Add a curve to the end of the sequence"""
    self.curves.append(curve)

get_data(data: BareMetadata, default: Any = nan, allow_default: bool = True, allow_property: bool = True) -> ndarray ¤

Concatenate curve metadata into an array of length n

Parameters:

Name Type Description Default
data BareMetadata

One of:

  • Name of the property stored in curve metadata
  • Name of a Curve attribute, if allow_property is true
  • A function Curve -> value
  • An length n array or list of values
required
allow_property bool

If true, data may be the name of a Curve attribute, such as 'area' or 'length'

True
allow_default bool

If true, and the requested data is only available on a subset of curves, return default for those curves.

True
default Any

The default value if named parameter data is not present in a curve's metadata. If not supplied, all curves in the collection must have that metadata parameter, otherwise a KeyError is raised.

nan
Source code in src\curvey\curves.py
def get_data(
    self,
    data: BareMetadata,
    default: Any = nan,
    allow_default: bool = True,
    allow_property: bool = True,
) -> ndarray:
    """Concatenate curve metadata into an array of length `n`

    Parameters
    ----------
    data
        One of:

        - Name of the property stored in curve metadata
        - Name of a `Curve` attribute, if `allow_property` is true
        - A function `Curve -> value`
        - An length `n` array or list of values

    allow_property
        If true, `data` may be the name of a Curve attribute, such as 'area' or 'length'

    allow_default
        If true, and the requested data is only available on a subset of curves, return
        `default` for those curves.

    default
        The default value if named parameter `data` is not present in a curve's metadata.
        If not supplied, all curves in the collection must have that metadata parameter,
        otherwise a `KeyError` is raised.

    """
    if isinstance(data, str):
        if data in self.keys("union"):
            if allow_default:
                return array([c.data.get(data, default) for c in self])

            if data in self.keys("intersection"):
                msg = f"Metadata '{data}' is only present on some curves"
                raise KeyError(msg)

            return array([c[data] for c in self])

        if hasattr(Curve, data):
            if allow_property:
                return array([getattr(c, data) for c in self])

            msg = (
                f"Metadata '{data}' not found. "
                "it's a Curve property but `allow_property` is False"
            )
            raise KeyError(msg)

        msg = f"Metadata '{data}' not found"
        raise KeyError(msg)

    if callable(data):
        return array([data(c) for c in self])

    if isinstance(data, Sequence):
        if (n := len(data)) != self.n:
            msg = f"Expected a sequence of length self.n = {self.n}, got {n}"
            raise ValueError(msg)
        return asarray(data)

    msg = f"Unrecognized data type {type(data)}"  # type: ignore [unreachable]
    raise TypeError(msg)

get_named_data(data: BareMetadata | NamedMetadata, **kwargs) -> tuple[str, ndarray] ¤

Get curve metadata (name, values) pairs

If data is just a name, return (name, values). If data is something that can reasonably interpreted as values, try to figure out a reasonable name for them.

Source code in src\curvey\curves.py
def get_named_data(self, data: BareMetadata | NamedMetadata, **kwargs) -> tuple[str, ndarray]:
    """Get curve metadata (name, values) pairs

    If `data` is just a name, return (name, values).
    If `data` is something that can reasonably interpreted as `values`,
    try to figure out a reasonable name for them.
    """
    if isinstance(data, str):
        return data, self.get_data(data, **kwargs)

    if isinstance(data, tuple) and len(data) == 2:
        name = data[0]
        if not isinstance(name, str):
            msg = f"Expected metadata name to be str, got {name}"
            raise TypeError(msg)
        return name, self.get_data(data[1], **kwargs)

    if callable(data):
        name = data.__name__ if hasattr(data, "__name__") else str(data)
        return name, self.get_data(data, **kwargs)

    name = type(data).__name__
    return name, self.get_data(data, **kwargs)

iter_curves(idx: str = 'idx') -> Iterator[Curve] ¤

Iterate over curves in the sequence

The index of the curve is stored in the idx metadata parameter. This might be useful for tracking the original index in a subset.

from curvey import Curves

orig = Curves(Curve.circle(3) for _ in range(6))
every_other = Curves(orig.iter_curves('orig_idx'))[::2]
every_other['orig_idx']
Source code in src\curvey\curves.py
def iter_curves(self, idx: str = "idx") -> Iterator[Curve]:
    """Iterate over curves in the sequence

    The index of the curve is stored in the `idx` metadata parameter. This might be useful for
    tracking the original index in a subset.

    ```python
    from curvey import Curves

    orig = Curves(Curve.circle(3) for _ in range(6))
    every_other = Curves(orig.iter_curves('orig_idx'))[::2]
    every_other['orig_idx']
    ```
    """
    for i, c in enumerate(self.curves):
        yield c.with_data(**{idx: i})

keys(mode: Literal['intersection', 'union'] = 'union') -> set[str] ¤

Unique curve metadata parameter names

Parameters:

Name Type Description Default
mode Literal['intersection', 'union']

If 'union', return keys that are present on any Curve in the Curves. If 'intersection', return only keys that are present on all Curves in the Curves.

'union'
Source code in src\curvey\curves.py
def keys(self, mode: Literal["intersection", "union"] = "union") -> set[str]:
    """Unique curve metadata parameter names

    Parameters
    ----------
    mode
        If 'union', return keys that are present on any `Curve` in the `Curves`.
        If 'intersection', return only keys that are present on all `Curve`s in the `Curves`.
    """
    if self.n == 0:
        return set()

    keys = (set(c.data.keys()) for c in self)
    if mode == "intersection":
        return set.intersection(*keys)

    if mode == "union":
        return set.union(*keys)

    raise ValueError(mode)

plot(y: Metadata, x: Metadata | None = None, ax: Axes | None = None, label_axes: bool | None = None, label: str | bool | None = True, **kwargs) ¤

Plot metadata values against each other.

By default, the independent variable y is 'time', if it's present.

Parameters:

Name Type Description Default
y Metadata

The name of the parameter to plot on the y-axis. Can also be a function Curve -> float, or a tuple(name, function), Or a tuple(name, array)

required
x Metadata | None

The name of the parameter to plot on the x-axis, or alternative type as described for y. If not supplied, defaults to time if it's present in the curve metadata, or index otherwise.

None
label_axes bool | None

If true, set x and y labels. Defaults to true if a new axes is created.

None
label str | bool | None

Name to label the plot with, for use in matplotlib legend. Defaults to the name of the y parameter.

True
ax Axes | None

The matplotlib axes to plot in. Defaults to the current axes.

None
**kwargs

Remaining kwargs are passed to matplotlib.pyplot.plot

{}
Source code in src\curvey\curves.py
def plot(
    self,
    y: Metadata,
    x: Metadata | None = None,
    ax: Axes | None = None,
    label_axes: bool | None = None,
    label: str | bool | None = True,
    **kwargs,
):
    """Plot metadata values against each other.

    By default, the independent variable `y` is 'time', if it's present.

    Parameters
    ----------
    y
        The name of the parameter to plot on the y-axis.
        Can also be a function `Curve` -> float, or a `tuple(name, function)`,
        Or a `tuple(name, array)`

    x
        The name of the parameter to plot on the x-axis, or alternative type as described for
        `y`. If not supplied, defaults to `time` if it's present in the curve metadata,
        or index otherwise.

    label_axes
        If true, set x and y labels. Defaults to true if a new axes is created.

    label
        Name to label the plot with, for use in matplotlib legend. Defaults to the name
        of the `y` parameter.

    ax
        The matplotlib axes to plot in. Defaults to the current axes.

    **kwargs
        Remaining kwargs are passed to `matplotlib.pyplot.plot`

    """
    if ax is None:
        ax = plt.gca()
        if label_axes is None:
            label_axes = True

    yname, ydata = self.get_named_data(y)

    if x is None:
        keys = self.keys(mode="union")
        if "time" in keys:
            x = "time"
        elif "idx" in keys:
            x = "idx"
        else:
            x = ("idx", arange(self.n))

    xname, xdata = self.get_named_data(x)

    if label_axes:
        ax.set_xlabel(xname)
        ax.set_ylabel(yname)

    if label and isinstance(label, bool):
        label = yname

    ax.plot(xdata, ydata, label=label, **kwargs)

subplots(n_rows: int | None = 1, n_cols: int | None = None, axis: str | None = 'scaled', show_axes=False, plot_fn: Callable[[Curve], None] | None = None, subtitle: str | Callable[[Curve], str] | None = None, share_xy=True, figsize: tuple[float, float] = (15, 5), idx: str = 'idx', axs: Sequence[Axes] | None = None, hide_unused: bool = True) ¤

Plot each curve in the sequence in a different subplot

Parameters:

Name Type Description Default
figsize tuple[float, float]

The size of the overall superfigure.

(15, 5)
n_rows int | None

Number of rows.

1
n_cols int | None

Number of columns. By default, n_cols = ceil(self.n / n_rows). Ifn_colsis specified, andn_rows*n_cols<self.n`, the curve sequence is automatically subsampled.

None
axis str | None

Argument to plt.axis. By default this is 'equal' (i.e., make circles circular).

'scaled'
show_axes

Whether to show each subplot axes, i.e. border and x/y ticks, etc.

False
plot_fn Callable[[Curve], None] | None

By default this just dispatches to Curve.plot_edges(directed=False).

None
subtitle str | Callable[[Curve], str] | None

A convenience argument to put a title over each subplot. If subtitle is a string, a title is constructed from the corresponding curve metadata. Otherwise, subtitle should be a function that accepts a curve and returns a string.

None
share_xy

Whether each subplot should share x/y limits.

True
idx str

The index of the curve in this collection is stored in the curve metadata property with this name.

'idx'
axs Sequence[Axes] | None

Array of matplotlib axes to use for subplots. If supplied, n_rows and n_cols are ignored and determined by the shape of this array.

None
hide_unused bool

If n_axs > n_plots, hide the unused axes.

True

Returns:

Name Type Description
axes

(n_rows, n_cols) array of matplotlib.axes.Axes objects

Source code in src\curvey\curves.py
def subplots(
    self,
    n_rows: int | None = 1,
    n_cols: int | None = None,
    axis: str | None = "scaled",
    show_axes=False,
    plot_fn: Callable[[Curve], None] | None = None,
    subtitle: str | Callable[[Curve], str] | None = None,
    share_xy=True,
    figsize: tuple[float, float] = (15, 5),
    idx: str = "idx",
    axs: Sequence[Axes] | None = None,
    hide_unused: bool = True,
):
    """Plot each curve in the sequence in a different subplot

    Parameters
    ----------
    figsize
        The size of the overall superfigure.

    n_rows
        Number of rows.

    n_cols
        Number of columns. By default, `n_cols` = ceil(self.n / n_rows)`. If `n_cols` is specified, and
        `n_rows` * `n_cols` < `self.n`, the curve sequence is automatically subsampled.

    axis
        Argument to `plt.axis`. By default this is 'equal' (i.e., make circles circular).

    show_axes
        Whether to show each subplot axes, i.e. border and x/y ticks, etc.

    plot_fn
        By default this just dispatches to `Curve.plot_edges(directed=False)`.

    subtitle
        A convenience argument to put a title over each subplot. If `subtitle` is a string,
        a title is constructed from the corresponding curve metadata. Otherwise, `subtitle`
        should be a function that accepts a curve and returns a string.

    share_xy
        Whether each subplot should share x/y limits.

    idx
        The index of the curve in this collection is stored in the curve metadata property
        with this name.

    axs
        Array of matplotlib axes to use for subplots. If supplied, `n_rows` and `n_cols`
        are ignored and determined by the shape of this array.

    hide_unused
        If n_axs > n_plots, hide the unused axes.

    Returns
    -------
    axes :
        `(n_rows, n_cols)` array of `matplotlib.axes.Axes` objects

    """
    if not plot_fn:

        def plot_fn(c: Curve):
            c.plot_edges(directed=False)

    if isinstance(subtitle, str):
        _key = subtitle

        def subtitle(c: Curve) -> str:
            return f"{_key} = {c[_key]}"

    b = self._subplots_builder(
        n_rows=n_rows,
        n_cols=n_cols,
        share_xy=share_xy,
        figsize=figsize,
        idx=idx,
        axs=axs,
    )
    if hide_unused and b.n_axs > b.n_plots:
        # Hide unused axes if there's e.g. 3x3 axes but only 8 curves to plot
        for i in range(b.n_plots, b.n_axs):
            b.get_ax(i).axis("off")

    for i in range(b.n_plots):
        b.axis_subplot(i=i, plot_fn=plot_fn, subtitle=subtitle, axis=axis, show_axes=show_axes)

    if b.fig is not None:  # If we created the figure
        b.fig.tight_layout()

    return b.axs

superimposed(ax: Axes | None = None, plot_fn: Callable[[Curve], Any] | None = None, color: Metadata | None = None, clim: tuple[float, float] | None = None, idx: str = 'idx') -> list[Any] ¤

Plot every curve in the same axes

Parameters:

Name Type Description Default
ax Axes | None

Matlplotlib axes to plot in. Default current axes.

None
plot_fn Callable[[Curve], Any] | None

Function Curve -> matplotlib_object to plot the curve. By default, this dispatches to curve.plot

None
color Metadata | None

The name of a curve metadata parameter to color by. If plot_fn is supplied, this is ignored.

None
clim tuple[float, float] | None

Range to scale color data to.

None
idx str

The index of the curve in this collection is stored in this curve's metadata.

'idx'

Returns:

Type Description
list[Any]

List of objects returned by plot_fn.

Source code in src\curvey\curves.py
def superimposed(
    self,
    ax: Axes | None = None,
    plot_fn: Callable[[Curve], Any] | None = None,
    color: Metadata | None = None,
    clim: tuple[float, float] | None = None,
    idx: str = "idx",
) -> list[Any]:
    """Plot every curve in the same axes

    Parameters
    ----------
    ax
        Matlplotlib axes to plot in. Default current axes.

    plot_fn
        Function `Curve -> matplotlib_object` to plot the curve.
        By default, this dispatches to `curve.plot`

    color
        The name of a curve metadata parameter to color by. If `plot_fn` is supplied,
        this is ignored.

    clim
        Range to scale color data to.

    idx
        The index of the curve in this collection is stored in this curve's metadata.

    Returns
    -------
    :
        List of objects returned by `plot_fn`.
    """

    ax = _get_ax(ax)

    if not plot_fn:
        if color is not None:
            cmap = plt.get_cmap("viridis")
            _cname, cdata = self.get_named_data(color)
            cnorm = plt.Normalize(*clim) if clim else plt.Normalize()
            cdata = cnorm(cdata)

            def plot_fn(c: Curve) -> Line2D:
                return c.plot(color=cmap(cdata[c[idx]]), ax=ax)
        else:

            def plot_fn(c: Curve) -> Line2D:
                return c.plot(ax=ax)

    out = []

    for curve in self.iter_curves(idx=idx):
        out.append(plot_fn(curve))

    return out