• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

python-control / python-control / 13106139066

03 Feb 2025 04:04AM UTC coverage: 94.709% (+0.2%) from 94.536%
13106139066

push

github

web-flow
Merge pull request #1118 from roryyorke/lint-lib-only

Lint library code only with `ruff check`

9630 of 10168 relevant lines covered (94.71%)

8.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.42
control/phaseplot.py
1
# phaseplot.py - generate 2D phase portraits
2
#
3
# Author: Richard M. Murray
4
# Date:   23 Mar 2024 (legacy version information below)
5
#
6
# TODO
7
# * Allow multiple timepoints (and change timespec name to T?)
8
# * Update linestyles (color -> linestyle?)
9
# * Check for keyword compatibility with other plot routines
10
# * Set up configuration parameters (nyquist --> phaseplot)
11

12
"""Module for generating 2D phase plane plots.
13

14
The :mod:`control.phaseplot` module contains functions for generating 2D
15
phase plots. The base function for creating phase plane portraits is
16
:func:`~control.phase_plane_plot`, which generates a phase plane portrait
17
for a 2 state I/O system (with no inputs).  In addition, several other
18
functions are available to create customized phase plane plots:
19

20
* boxgrid: Generate a list of points along the edge of a box
21
* circlegrid: Generate list of points around a circle
22
* equilpoints: Plot equilibrium points in the phase plane
23
* meshgrid: Generate a list of points forming a mesh
24
* separatrices: Plot separatrices in the phase plane
25
* streamlines: Plot stream lines in the phase plane
26
* vectorfield: Plot a vector field in the phase plane
27

28
"""
29

30
import math
9✔
31
import warnings
9✔
32

33
import matplotlib as mpl
9✔
34
import matplotlib.pyplot as plt
9✔
35
import numpy as np
9✔
36
from scipy.integrate import odeint
9✔
37

38
from . import config
9✔
39
from .ctrlplot import ControlPlot, _add_arrows_to_line2D, _get_color, \
9✔
40
    _process_ax_keyword, _update_plot_title
41
from .exception import ControlArgument
9✔
42
from .nlsys import NonlinearIOSystem, find_operating_point, \
9✔
43
    input_output_response
44

45
__all__ = ['phase_plane_plot', 'phase_plot', 'box_grid']
9✔
46

47
# Default values for module parameter variables
48
_phaseplot_defaults = {
9✔
49
    'phaseplot.arrows': 2,                  # number of arrows around curve
50
    'phaseplot.arrow_size': 8,              # pixel size for arrows
51
    'phaseplot.separatrices_radius': 0.1    # initial radius for separatrices
52
}
53

54
def phase_plane_plot(
9✔
55
        sys, pointdata=None, timedata=None, gridtype=None, gridspec=None,
56
        plot_streamlines=True, plot_vectorfield=False, plot_equilpoints=True,
57
        plot_separatrices=True, ax=None, suppress_warnings=False, title=None,
58
        **kwargs
59
):
60
    """Plot phase plane diagram.
61

62
    This function plots phase plane data, including vector fields, stream
63
    lines, equilibrium points, and contour curves.
64

65
    Parameters
66
    ----------
67
    sys : NonlinearIOSystem or callable(t, x, ...)
68
        I/O system or function used to generate phase plane data. If a
69
        function is given, the remaining arguments are drawn from the
70
        `params` keyword.
71
    pointdata : list or 2D array
72
        List of the form [xmin, xmax, ymin, ymax] describing the
73
        boundaries of the phase plot or an array of shape (N, 2)
74
        giving points of at which to plot the vector field.
75
    timedata : int or list of int
76
        Time to simulate each streamline.  If a list is given, a different
77
        time can be used for each initial condition in `pointdata`.
78
    gridtype : str, optional
79
        The type of grid to use for generating initial conditions:
80
        'meshgrid' (default) generates a mesh of initial conditions within
81
        the specified boundaries, 'boxgrid' generates initial conditions
82
        along the edges of the boundary, 'circlegrid' generates a circle of
83
        initial conditions around each point in point data.
84
    gridspec : list, optional
85
        If the gridtype is 'meshgrid' and 'boxgrid', `gridspec` gives the
86
        size of the grid in the x and y axes on which to generate points.
87
        If gridtype is 'circlegrid', then `gridspec` is a 2-tuple
88
        specifying the radius and number of points around each point in the
89
        `pointdata` array.
90
    params : dict, optional
91
        Parameters to pass to system. For an I/O system, `params` should be
92
        a dict of parameters and values. For a callable, `params` should be
93
        dict with key 'args' and value given by a tuple (passed to callable).
94
    color : matplotlib color spec, optional
95
        Plot all elements in the given color (use `plot_<fcn>={'color': c}`
96
        to set the color in one element of the phase plot.
97
    ax : matplotlib.axes.Axes, optional
98
        The matplotlib axes to draw the figure on.  If not specified and
99
        the current figure has a single axes, that axes is used.
100
        Otherwise, a new figure is created.
101

102
    Returns
103
    -------
104
    cplt : :class:`ControlPlot` object
105
        Object containing the data that were plotted:
106

107
          * cplt.lines: array of list of :class:`matplotlib.artist.Artist`
108
            objects:
109

110
              - lines[0] = list of Line2D objects (streamlines, separatrices).
111
              - lines[1] = Quiver object (vector field arrows).
112
              - lines[2] = list of Line2D objects (equilibrium points).
113

114
          * cplt.axes: 2D array of :class:`matplotlib.axes.Axes` for the plot.
115

116
          * cplt.figure: :class:`matplotlib.figure.Figure` containing the plot.
117

118
        See :class:`ControlPlot` for more detailed information.
119

120

121
    Other parameters
122
    ----------------
123
    dir : str, optional
124
        Direction to draw streamlines: 'forward' to flow forward in time
125
        from the reference points, 'reverse' to flow backward in time, or
126
        'both' to flow both forward and backward.  The amount of time to
127
        simulate in each direction is given by the ``timedata`` argument.
128
    plot_streamlines : bool or dict, optional
129
        If `True` (default) then plot streamlines based on the pointdata
130
        and gridtype.  If set to a dict, pass on the key-value pairs in
131
        the dict as keywords to :func:`~control.phaseplot.streamlines`.
132
    plot_vectorfield : bool or dict, optional
133
        If `True` (default) then plot the vector field based on the pointdata
134
        and gridtype.  If set to a dict, pass on the key-value pairs in
135
        the dict as keywords to :func:`~control.phaseplot.vectorfield`.
136
    plot_equilpoints : bool or dict, optional
137
        If `True` (default) then plot equilibrium points based in the phase
138
        plot boundary. If set to a dict, pass on the key-value pairs in the
139
        dict as keywords to :func:`~control.phaseplot.equilpoints`.
140
    plot_separatrices : bool or dict, optional
141
        If `True` (default) then plot separatrices starting from each
142
        equilibrium point.  If set to a dict, pass on the key-value pairs
143
        in the dict as keywords to :func:`~control.phaseplot.separatrices`.
144
    rcParams : dict
145
        Override the default parameters used for generating plots.
146
        Default is set by config.default['ctrlplot.rcParams'].
147
    suppress_warnings : bool, optional
148
        If set to `True`, suppress warning messages in generating trajectories.
149
    title : str, optional
150
        Set the title of the plot.  Defaults to plot type and system name(s).
151

152
    """
153
    # Process arguments
154
    params = kwargs.get('params', None)
9✔
155
    sys = _create_system(sys, params)
9✔
156
    pointdata = [-1, 1, -1, 1] if pointdata is None else pointdata
9✔
157
    rcParams = config._get_param('ctrlplot', 'rcParams', kwargs, pop=True)
9✔
158

159
    # Create axis if needed
160
    user_ax = ax
9✔
161
    fig, ax = _process_ax_keyword(user_ax, squeeze=True, rcParams=rcParams)
9✔
162

163
    # Create copy of kwargs for later checking to find unused arguments
164
    initial_kwargs = dict(kwargs)
9✔
165

166
    # Utility function to create keyword arguments
167
    def _create_kwargs(global_kwargs, local_kwargs, **other_kwargs):
9✔
168
        new_kwargs = dict(global_kwargs)
9✔
169
        new_kwargs.update(other_kwargs)
9✔
170
        if isinstance(local_kwargs, dict):
9✔
171
            new_kwargs.update(local_kwargs)
9✔
172
        return new_kwargs
9✔
173

174
    # Create list for storing outputs
175
    out = np.array([[], None, None], dtype=object)
9✔
176

177
    # Plot out the main elements
178
    if plot_streamlines:
9✔
179
        kwargs_local = _create_kwargs(
9✔
180
            kwargs, plot_streamlines, gridspec=gridspec, gridtype=gridtype,
181
            ax=ax)
182
        out[0] += streamlines(
9✔
183
            sys, pointdata, timedata, _check_kwargs=False,
184
            suppress_warnings=suppress_warnings, **kwargs_local)
185

186
        # Get rid of keyword arguments handled by streamlines
187
        for kw in ['arrows', 'arrow_size', 'arrow_style', 'color',
9✔
188
                   'dir', 'params']:
189
            initial_kwargs.pop(kw, None)
9✔
190

191
    # Reset the gridspec for the remaining commands, if needed
192
    if gridtype not in [None, 'boxgrid', 'meshgrid']:
9✔
193
        gridspec = None
×
194

195
    if plot_separatrices:
9✔
196
        kwargs_local = _create_kwargs(
9✔
197
            kwargs, plot_separatrices, gridspec=gridspec, ax=ax)
198
        out[0] += separatrices(
9✔
199
            sys, pointdata, _check_kwargs=False, **kwargs_local)
200

201
        # Get rid of keyword arguments handled by separatrices
202
        for kw in ['arrows', 'arrow_size', 'arrow_style', 'params']:
9✔
203
            initial_kwargs.pop(kw, None)
9✔
204

205
    if plot_vectorfield:
9✔
206
        kwargs_local = _create_kwargs(
×
207
            kwargs, plot_vectorfield, gridspec=gridspec, ax=ax)
208
        out[1] = vectorfield(
×
209
            sys, pointdata, _check_kwargs=False, **kwargs_local)
210

211
        # Get rid of keyword arguments handled by vectorfield
212
        for kw in ['color', 'params']:
×
213
            initial_kwargs.pop(kw, None)
×
214

215
    if plot_equilpoints:
9✔
216
        kwargs_local = _create_kwargs(
9✔
217
            kwargs, plot_equilpoints, gridspec=gridspec, ax=ax)
218
        out[2] = equilpoints(
9✔
219
            sys, pointdata, _check_kwargs=False, **kwargs_local)
220

221
        # Get rid of keyword arguments handled by equilpoints
222
        for kw in ['params']:
9✔
223
            initial_kwargs.pop(kw, None)
9✔
224

225
    # Make sure all keyword arguments were used
226
    if initial_kwargs:
9✔
227
        raise TypeError("unrecognized keywords: ", str(initial_kwargs))
9✔
228

229
    if user_ax is None:
9✔
230
        if title is None:
9✔
231
            title = f"Phase portrait for {sys.name}"
9✔
232
        _update_plot_title(title, use_existing=False, rcParams=rcParams)
9✔
233
        ax.set_xlabel(sys.state_labels[0])
9✔
234
        ax.set_ylabel(sys.state_labels[1])
9✔
235
        plt.tight_layout()
9✔
236

237
    return ControlPlot(out, ax, fig)
9✔
238

239

240
def vectorfield(
9✔
241
        sys, pointdata, gridspec=None, ax=None, suppress_warnings=False,
242
        _check_kwargs=True, **kwargs):
243
    """Plot a vector field in the phase plane.
244

245
    This function plots a vector field for a two-dimensional state
246
    space system.
247

248
    Parameters
249
    ----------
250
    sys : NonlinearIOSystem or callable(t, x, ...)
251
        I/O system or function used to generate phase plane data.  If a
252
        function is given, the remaining arguments are drawn from the
253
        `params` keyword.
254
    pointdata : list or 2D array
255
        List of the form [xmin, xmax, ymin, ymax] describing the
256
        boundaries of the phase plot or an array of shape (N, 2)
257
        giving points of at which to plot the vector field.
258
    gridtype : str, optional
259
        The type of grid to use for generating initial conditions:
260
        'meshgrid' (default) generates a mesh of initial conditions within
261
        the specified boundaries, 'boxgrid' generates initial conditions
262
        along the edges of the boundary, 'circlegrid' generates a circle of
263
        initial conditions around each point in point data.
264
    gridspec : list, optional
265
        If the gridtype is 'meshgrid' and 'boxgrid', `gridspec` gives the
266
        size of the grid in the x and y axes on which to generate points.
267
        If gridtype is 'circlegrid', then `gridspec` is a 2-tuple
268
        specifying the radius and number of points around each point in the
269
        `pointdata` array.
270
    params : dict or list, optional
271
        Parameters to pass to system. For an I/O system, `params` should be
272
        a dict of parameters and values. For a callable, `params` should be
273
        dict with key 'args' and value given by a tuple (passed to callable).
274
    color : matplotlib color spec, optional
275
        Plot the vector field in the given color.
276
    ax : matplotlib.axes.Axes
277
        Use the given axes for the plot, otherwise use the current axes.
278

279
    Returns
280
    -------
281
    out : Quiver
282

283
    Other parameters
284
    ----------------
285
    rcParams : dict
286
        Override the default parameters used for generating plots.
287
        Default is set by config.default['ctrlplot.rcParams'].
288
    suppress_warnings : bool, optional
289
        If set to `True`, suppress warning messages in generating trajectories.
290

291
    """
292
    # Process keywords
293
    rcParams = config._get_param('ctrlplot', 'rcParams', kwargs, pop=True)
9✔
294

295
    # Get system parameters
296
    params = kwargs.pop('params', None)
9✔
297

298
    # Create system from callable, if needed
299
    sys = _create_system(sys, params)
9✔
300

301
    # Determine the points on which to generate the vector field
302
    points, _ = _make_points(pointdata, gridspec, 'meshgrid')
9✔
303

304
    # Create axis if needed
305
    if ax is None:
9✔
306
        ax = plt.gca()
9✔
307

308
    # Set the plotting limits
309
    xlim, ylim, maxlim = _set_axis_limits(ax, pointdata)
9✔
310

311
    # Figure out the color to use
312
    color = _get_color(kwargs, ax=ax)
9✔
313

314
    # Make sure all keyword arguments were processed
315
    if _check_kwargs and kwargs:
9✔
316
        raise TypeError("unrecognized keywords: ", str(kwargs))
9✔
317

318
    # Generate phase plane (quiver) data
319
    vfdata = np.zeros((points.shape[0], 4))
9✔
320
    sys._update_params(params)
9✔
321
    for i, x in enumerate(points):
9✔
322
        vfdata[i, :2] = x
9✔
323
        vfdata[i, 2:] = sys._rhs(0, x, np.zeros(sys.ninputs))
9✔
324

325
    with plt.rc_context(rcParams):
9✔
326
        out = ax.quiver(
9✔
327
            vfdata[:, 0], vfdata[:, 1], vfdata[:, 2], vfdata[:, 3],
328
            angles='xy', color=color)
329

330
    return out
9✔
331

332

333
def streamlines(
9✔
334
        sys, pointdata, timedata=1, gridspec=None, gridtype=None, dir=None,
335
        ax=None, _check_kwargs=True, suppress_warnings=False, **kwargs):
336
    """Plot stream lines in the phase plane.
337

338
    This function plots stream lines for a two-dimensional state space
339
    system.
340

341
    Parameters
342
    ----------
343
    sys : NonlinearIOSystem or callable(t, x, ...)
344
        I/O system or function used to generate phase plane data.  If a
345
        function is given, the remaining arguments are drawn from the
346
        `params` keyword.
347
    pointdata : list or 2D array
348
        List of the form [xmin, xmax, ymin, ymax] describing the
349
        boundaries of the phase plot or an array of shape (N, 2)
350
        giving points of at which to plot the vector field.
351
    timedata : int or list of int
352
        Time to simulate each streamline.  If a list is given, a different
353
        time can be used for each initial condition in `pointdata`.
354
    gridtype : str, optional
355
        The type of grid to use for generating initial conditions:
356
        'meshgrid' (default) generates a mesh of initial conditions within
357
        the specified boundaries, 'boxgrid' generates initial conditions
358
        along the edges of the boundary, 'circlegrid' generates a circle of
359
        initial conditions around each point in point data.
360
    gridspec : list, optional
361
        If the gridtype is 'meshgrid' and 'boxgrid', `gridspec` gives the
362
        size of the grid in the x and y axes on which to generate points.
363
        If gridtype is 'circlegrid', then `gridspec` is a 2-tuple
364
        specifying the radius and number of points around each point in the
365
        `pointdata` array.
366
    dir : str, optional
367
        Direction to draw streamlines: 'forward' to flow forward in time
368
        from the reference points, 'reverse' to flow backward in time, or
369
        'both' to flow both forward and backward.  The amount of time to
370
        simulate in each direction is given by the ``timedata`` argument.
371
    params : dict or list, optional
372
        Parameters to pass to system. For an I/O system, `params` should be
373
        a dict of parameters and values. For a callable, `params` should be
374
        dict with key 'args' and value given by a tuple (passed to callable).
375
    color : str
376
        Plot the streamlines in the given color.
377
    ax : matplotlib.axes.Axes
378
        Use the given axes for the plot, otherwise use the current axes.
379

380
    Returns
381
    -------
382
    out : list of Line2D objects
383

384
    Other parameters
385
    ----------------
386
    rcParams : dict
387
        Override the default parameters used for generating plots.
388
        Default is set by config.default['ctrlplot.rcParams'].
389
    suppress_warnings : bool, optional
390
        If set to `True`, suppress warning messages in generating trajectories.
391

392
    """
393
    # Process keywords
394
    rcParams = config._get_param('ctrlplot', 'rcParams', kwargs, pop=True)
9✔
395

396
    # Get system parameters
397
    params = kwargs.pop('params', None)
9✔
398

399
    # Create system from callable, if needed
400
    sys = _create_system(sys, params)
9✔
401

402
    # Parse the arrows keyword
403
    arrow_pos, arrow_style = _parse_arrow_keywords(kwargs)
9✔
404

405
    # Determine the points on which to generate the streamlines
406
    points, gridspec = _make_points(pointdata, gridspec, gridtype=gridtype)
9✔
407
    if dir is None:
9✔
408
        dir = 'both' if gridtype == 'meshgrid' else 'forward'
9✔
409

410
    # Create axis if needed
411
    if ax is None:
9✔
412
        ax = plt.gca()
9✔
413

414
    # Set the axis limits
415
    xlim, ylim, maxlim = _set_axis_limits(ax, pointdata)
9✔
416

417
    # Figure out the color to use
418
    color = _get_color(kwargs, ax=ax)
9✔
419

420
    # Make sure all keyword arguments were processed
421
    if _check_kwargs and kwargs:
9✔
422
        raise TypeError("unrecognized keywords: ", str(kwargs))
9✔
423

424
    # Create reverse time system, if needed
425
    if dir != 'forward':
9✔
426
        revsys = NonlinearIOSystem(
9✔
427
            lambda t, x, u, params: -np.asarray(sys.updfcn(t, x, u, params)),
428
            sys.outfcn, states=sys.nstates, inputs=sys.ninputs,
429
            outputs=sys.noutputs, params=sys.params)
430
    else:
431
        revsys = None
9✔
432

433
    # Generate phase plane (streamline) data
434
    out = []
9✔
435
    for i, X0 in enumerate(points):
9✔
436
        # Create the trajectory for this point
437
        timepts = _make_timepts(timedata, i)
9✔
438
        traj = _create_trajectory(
9✔
439
            sys, revsys, timepts, X0, params, dir,
440
            gridtype=gridtype, gridspec=gridspec, xlim=xlim, ylim=ylim,
441
            suppress_warnings=suppress_warnings)
442

443
        # Plot the trajectory (if there is one)
444
        if traj.shape[1] > 1:
9✔
445
            with plt.rc_context(rcParams):
9✔
446
                out += ax.plot(traj[0], traj[1], color=color)
9✔
447

448
                # Add arrows to the lines at specified intervals
449
                _add_arrows_to_line2D(
9✔
450
                    ax, out[-1], arrow_pos, arrowstyle=arrow_style, dir=1)
451
    return out
9✔
452

453

454
def equilpoints(
9✔
455
        sys, pointdata, gridspec=None, color='k', ax=None, _check_kwargs=True,
456
        **kwargs):
457
    """Plot equilibrium points in the phase plane.
458

459
    This function plots the equilibrium points for a planar dynamical system.
460

461
    Parameters
462
    ----------
463
    sys : NonlinearIOSystem or callable(t, x, ...)
464
        I/O system or function used to generate phase plane data. If a
465
        function is given, the remaining arguments are drawn from the
466
        `params` keyword.
467
    pointdata : list or 2D array
468
        List of the form [xmin, xmax, ymin, ymax] describing the
469
        boundaries of the phase plot or an array of shape (N, 2)
470
        giving points of at which to plot the vector field.
471
    gridtype : str, optional
472
        The type of grid to use for generating initial conditions:
473
        'meshgrid' (default) generates a mesh of initial conditions within
474
        the specified boundaries, 'boxgrid' generates initial conditions
475
        along the edges of the boundary, 'circlegrid' generates a circle of
476
        initial conditions around each point in point data.
477
    gridspec : list, optional
478
        If the gridtype is 'meshgrid' and 'boxgrid', `gridspec` gives the
479
        size of the grid in the x and y axes on which to generate points.
480
        If gridtype is 'circlegrid', then `gridspec` is a 2-tuple
481
        specifying the radius and number of points around each point in the
482
        `pointdata` array.
483
    params : dict or list, optional
484
        Parameters to pass to system. For an I/O system, `params` should be
485
        a dict of parameters and values. For a callable, `params` should be
486
        dict with key 'args' and value given by a tuple (passed to callable).
487
    color : str
488
        Plot the equilibrium points in the given color.
489
    ax : matplotlib.axes.Axes
490
        Use the given axes for the plot, otherwise use the current axes.
491

492
    Returns
493
    -------
494
    out : list of Line2D objects
495

496
    Other parameters
497
    ----------------
498
    rcParams : dict
499
        Override the default parameters used for generating plots.
500
        Default is set by config.default['ctrlplot.rcParams'].
501

502
    """
503
    # Process keywords
504
    rcParams = config._get_param('ctrlplot', 'rcParams', kwargs, pop=True)
9✔
505

506
    # Get system parameters
507
    params = kwargs.pop('params', None)
9✔
508

509
    # Create system from callable, if needed
510
    sys = _create_system(sys, params)
9✔
511

512
    # Create axis if needed
513
    if ax is None:
9✔
514
        ax = plt.gca()
9✔
515

516
    # Set the axis limits
517
    xlim, ylim, maxlim = _set_axis_limits(ax, pointdata)
9✔
518

519
    # Determine the points on which to generate the vector field
520
    gridspec = [5, 5] if gridspec is None else gridspec
9✔
521
    points, _ = _make_points(pointdata, gridspec, 'meshgrid')
9✔
522

523
    # Make sure all keyword arguments were processed
524
    if _check_kwargs and kwargs:
9✔
525
        raise TypeError("unrecognized keywords: ", str(kwargs))
9✔
526

527
    # Search for equilibrium points
528
    equilpts = _find_equilpts(sys, points, params=params)
9✔
529

530
    # Plot the equilibrium points
531
    out = []
9✔
532
    for xeq in equilpts:
9✔
533
        with plt.rc_context(rcParams):
9✔
534
            out += ax.plot(xeq[0], xeq[1], marker='o', color=color)
9✔
535
    return out
9✔
536

537

538
def separatrices(
9✔
539
        sys, pointdata, timedata=None, gridspec=None, ax=None,
540
        _check_kwargs=True, suppress_warnings=False, **kwargs):
541
    """Plot separatrices in the phase plane.
542

543
    This function plots separatrices for a two-dimensional state space
544
    system.
545

546
    Parameters
547
    ----------
548
    sys : NonlinearIOSystem or callable(t, x, ...)
549
        I/O system or function used to generate phase plane data. If a
550
        function is given, the remaining arguments are drawn from the
551
        `params` keyword.
552
    pointdata : list or 2D array
553
        List of the form [xmin, xmax, ymin, ymax] describing the
554
        boundaries of the phase plot or an array of shape (N, 2)
555
        giving points of at which to plot the vector field.
556
    timedata : int or list of int
557
        Time to simulate each streamline.  If a list is given, a different
558
        time can be used for each initial condition in `pointdata`.
559
    gridtype : str, optional
560
        The type of grid to use for generating initial conditions:
561
        'meshgrid' (default) generates a mesh of initial conditions within
562
        the specified boundaries, 'boxgrid' generates initial conditions
563
        along the edges of the boundary, 'circlegrid' generates a circle of
564
        initial conditions around each point in point data.
565
    gridspec : list, optional
566
        If the gridtype is 'meshgrid' and 'boxgrid', `gridspec` gives the
567
        size of the grid in the x and y axes on which to generate points.
568
        If gridtype is 'circlegrid', then `gridspec` is a 2-tuple
569
        specifying the radius and number of points around each point in the
570
        `pointdata` array.
571
    params : dict or list, optional
572
        Parameters to pass to system. For an I/O system, `params` should be
573
        a dict of parameters and values. For a callable, `params` should be
574
        dict with key 'args' and value given by a tuple (passed to callable).
575
    color : matplotlib color spec, optional
576
        Plot the separatrics in the given color.  If a single color
577
        specification is given, this is used for both stable and unstable
578
        separatrices.  If a tuple is given, the first element is used as
579
        the color specification for stable separatrices and the second
580
        elmeent for unstable separatrices.
581
    ax : matplotlib.axes.Axes
582
        Use the given axes for the plot, otherwise use the current axes.
583

584
    Returns
585
    -------
586
    out : list of Line2D objects
587

588
    Other parameters
589
    ----------------
590
    rcParams : dict
591
        Override the default parameters used for generating plots.
592
        Default is set by config.default['ctrlplot.rcParams'].
593
    suppress_warnings : bool, optional
594
        If set to `True`, suppress warning messages in generating trajectories.
595

596
    """
597
    # Process keywords
598
    rcParams = config._get_param('ctrlplot', 'rcParams', kwargs, pop=True)
9✔
599

600
    # Get system parameters
601
    params = kwargs.pop('params', None)
9✔
602

603
    # Create system from callable, if needed
604
    sys = _create_system(sys, params)
9✔
605

606
    # Parse the arrows keyword
607
    arrow_pos, arrow_style = _parse_arrow_keywords(kwargs)
9✔
608

609
    # Determine the initial states to use in searching for equilibrium points
610
    gridspec = [5, 5] if gridspec is None else gridspec
9✔
611
    points, _ = _make_points(pointdata, gridspec, 'meshgrid')
9✔
612

613
    # Find the equilibrium points
614
    equilpts = _find_equilpts(sys, points, params=params)
9✔
615
    radius = config._get_param('phaseplot', 'separatrices_radius')
9✔
616

617
    # Create axis if needed
618
    if ax is None:
9✔
619
        ax = plt.gca()
9✔
620

621
    # Set the axis limits
622
    xlim, ylim, maxlim = _set_axis_limits(ax, pointdata)
9✔
623

624
    # Figure out the color to use for stable, unstable subspaces
625
    color = _get_color(kwargs)
9✔
626
    match color:
9✔
627
        case None:
9✔
628
            stable_color = 'r'
9✔
629
            unstable_color = 'b'
9✔
630
        case (stable_color, unstable_color) | [stable_color, unstable_color]:
9✔
631
            pass
9✔
632
        case single_color:
9✔
633
            stable_color = unstable_color = single_color
9✔
634

635
    # Make sure all keyword arguments were processed
636
    if _check_kwargs and kwargs:
9✔
637
        raise TypeError("unrecognized keywords: ", str(kwargs))
9✔
638

639
    # Create a "reverse time" system to use for simulation
640
    revsys = NonlinearIOSystem(
9✔
641
        lambda t, x, u, params: -np.array(sys.updfcn(t, x, u, params)),
642
        sys.outfcn, states=sys.nstates, inputs=sys.ninputs,
643
        outputs=sys.noutputs, params=sys.params)
644

645
    # Plot separatrices by flowing backwards in time along eigenspaces
646
    out = []
9✔
647
    for i, xeq in enumerate(equilpts):
9✔
648
        # Plot the equilibrium points
649
        with plt.rc_context(rcParams):
9✔
650
            out += ax.plot(xeq[0], xeq[1], marker='o', color='k')
9✔
651

652
        # Figure out the linearization and eigenvectors
653
        evals, evecs = np.linalg.eig(sys.linearize(xeq, 0, params=params).A)
9✔
654

655
        # See if we have real eigenvalues (=> evecs are meaningful)
656
        if evals[0].imag > 0:
9✔
657
            continue
9✔
658

659
        # Create default list of time points
660
        if timedata is not None:
9✔
661
            timepts = _make_timepts(timedata, i)
9✔
662

663
        # Generate the traces
664
        for j, dir in enumerate(evecs.T):
9✔
665
            # Figure out time vector if not yet computed
666
            if timedata is None:
9✔
667
                timescale = math.log(maxlim / radius) / abs(evals[j].real)
9✔
668
                timepts = np.linspace(0, timescale)
9✔
669

670
            # Run the trajectory starting in eigenvector directions
671
            for eps in [-radius, radius]:
9✔
672
                x0 = xeq + dir * eps
9✔
673
                if evals[j].real < 0:
9✔
674
                    traj = _create_trajectory(
9✔
675
                        sys, revsys, timepts, x0, params, 'reverse',
676
                        gridtype='boxgrid', xlim=xlim, ylim=ylim,
677
                        suppress_warnings=suppress_warnings)
678
                    color = stable_color
9✔
679
                    linestyle = '--'
9✔
680
                elif evals[j].real > 0:
9✔
681
                    traj = _create_trajectory(
9✔
682
                        sys, revsys, timepts, x0, params, 'forward',
683
                        gridtype='boxgrid', xlim=xlim, ylim=ylim,
684
                        suppress_warnings=suppress_warnings)
685
                    color = unstable_color
9✔
686
                    linestyle = '-'
9✔
687

688
                # Plot the trajectory (if there is one)
689
                if traj.shape[1] > 1:
9✔
690
                    with plt.rc_context(rcParams):
9✔
691
                        out += ax.plot(
9✔
692
                            traj[0], traj[1], color=color, linestyle=linestyle)
693

694
                    # Add arrows to the lines at specified intervals
695
                    with plt.rc_context(rcParams):
9✔
696
                        _add_arrows_to_line2D(
9✔
697
                            ax, out[-1], arrow_pos, arrowstyle=arrow_style,
698
                            dir=1)
699
    return out
9✔
700

701

702
#
703
# User accessible utility functions
704
#
705

706
# Utility function to generate boxgrid (in the form needed here)
707
def boxgrid(xvals, yvals):
9✔
708
    """Generate list of points along the edge of box.
709

710
    points = boxgrid(xvals, yvals) generates a list of points that
711
    corresponds to a grid given by the cross product of the x and y values.
712

713
    Parameters
714
    ----------
715
    xvals, yvals : 1D array-like
716
        Array of points defining the points on the lower and left edges of
717
        the box.
718

719
    Returns
720
    -------
721
    grid : 2D array
722
        Array with shape (p, 2) defining the points along the edges of the
723
        box, where p is the number of points around the edge.
724

725
    """
726
    return np.array(
9✔
727
        [(x, yvals[0]) for x in xvals[:-1]] +           # lower edge
728
        [(xvals[-1], y) for y in yvals[:-1]] +          # right edge
729
        [(x, yvals[-1]) for x in xvals[:0:-1]] +        # upper edge
730
        [(xvals[0], y) for y in yvals[:0:-1]]           # left edge
731
    )
732

733

734
# Utility function to generate meshgrid (in the form needed here)
735
# TODO: add examples of using grid functions directly
736
def meshgrid(xvals, yvals):
9✔
737
    """Generate list of points forming a mesh.
738

739
    points = meshgrid(xvals, yvals) generates a list of points that
740
    corresponds to a grid given by the cross product of the x and y values.
741

742
    Parameters
743
    ----------
744
    xvals, yvals : 1D array-like
745
        Array of points defining the points on the lower and left edges of
746
        the box.
747

748
    Returns
749
    -------
750
    grid: 2D array
751
        Array of points with shape (n * m, 2) defining the mesh
752

753
    """
754
    xvals, yvals = np.meshgrid(xvals, yvals)
9✔
755
    grid = np.zeros((xvals.shape[0] * xvals.shape[1], 2))
9✔
756
    grid[:, 0] = xvals.reshape(-1)
9✔
757
    grid[:, 1] = yvals.reshape(-1)
9✔
758

759
    return grid
9✔
760

761

762
# Utility function to generate circular grid
763
def circlegrid(centers, radius, num):
9✔
764
    """Generate list of points around a circle.
765

766
    points = circlegrid(centers, radius, num) generates a list of points
767
    that form a circle around a list of centers.
768

769
    Parameters
770
    ----------
771
    centers : 2D array-like
772
        Array of points with shape (p, 2) defining centers of the circles.
773
    radius : float
774
        Radius of the points to be generated around each center.
775
    num : int
776
        Number of points to generate around the circle.
777

778
    Returns
779
    -------
780
    grid: 2D array
781
        Array of points with shape (p * num, 2) defining the circles.
782

783
    """
784
    centers = np.atleast_2d(np.array(centers))
9✔
785
    grid = np.zeros((centers.shape[0] * num, 2))
9✔
786
    for i, center in enumerate(centers):
9✔
787
        grid[i * num: (i + 1) * num, :] = center + np.array([
9✔
788
            [radius * math.cos(theta), radius * math.sin(theta)] for
789
            theta in np.linspace(0, 2 * math.pi, num, endpoint=False)])
790
    return grid
9✔
791

792
#
793
# Internal utility functions
794
#
795

796
# Create a system from a callable
797
def _create_system(sys, params):
9✔
798
    if isinstance(sys, NonlinearIOSystem):
9✔
799
        if sys.nstates != 2:
9✔
800
            raise ValueError("system must be planar")
9✔
801
        return sys
9✔
802

803
    # Make sure that if params is present, it has 'args' key
804
    if params and not params.get('args', None):
9✔
805
        raise ValueError("params must be dict with key 'args'")
9✔
806

807
    _update = lambda t, x, u, params: sys(t, x, *params.get('args', ()))
9✔
808
    _output = lambda t, x, u, params: np.array([])
9✔
809
    return NonlinearIOSystem(
9✔
810
        _update, _output, states=2, inputs=0, outputs=0, name="_callable")
811

812
# Set axis limits for the plot
813
def _set_axis_limits(ax, pointdata):
9✔
814
    # Get the current axis limits
815
    if ax.lines:
9✔
816
        xlim, ylim = ax.get_xlim(), ax.get_ylim()
9✔
817
    else:
818
        # Nothing on the plot => always use new limits
819
        xlim, ylim = [np.inf, -np.inf], [np.inf, -np.inf]
9✔
820

821
    # Short utility function for updating axis limits
822
    def _update_limits(cur, new):
9✔
823
        return [min(cur[0], np.min(new)), max(cur[1], np.max(new))]
9✔
824

825
    # If we were passed a box, use that to update the limits
826
    if isinstance(pointdata, list) and len(pointdata) == 4:
9✔
827
        xlim = _update_limits(xlim, [pointdata[0], pointdata[1]])
9✔
828
        ylim = _update_limits(ylim, [pointdata[2], pointdata[3]])
9✔
829

830
    elif isinstance(pointdata, np.ndarray):
9✔
831
        pointdata = np.atleast_2d(pointdata)
9✔
832
        xlim = _update_limits(
9✔
833
            xlim, [np.min(pointdata[:, 0]), np.max(pointdata[:, 0])])
834
        ylim = _update_limits(
9✔
835
            ylim, [np.min(pointdata[:, 1]), np.max(pointdata[:, 1])])
836

837
    # Keep track of the largest dimension on the plot
838
    maxlim = max(xlim[1] - xlim[0], ylim[1] - ylim[0])
9✔
839

840
    # Set the new limits
841
    ax.autoscale(enable=True, axis='x', tight=True)
9✔
842
    ax.autoscale(enable=True, axis='y', tight=True)
9✔
843
    ax.set_xlim(xlim)
9✔
844
    ax.set_ylim(ylim)
9✔
845

846
    return xlim, ylim, maxlim
9✔
847

848

849
# Find equilibrium points
850
def _find_equilpts(sys, points, params=None):
9✔
851
    equilpts = []
9✔
852
    for i, x0 in enumerate(points):
9✔
853
        # Look for an equilibrium point near this point
854
        xeq, ueq = find_operating_point(sys, x0, 0, params=params)
9✔
855

856
        if xeq is None:
9✔
857
            continue            # didn't find anything
9✔
858

859
        # See if we have already found this point
860
        seen = False
9✔
861
        for x in equilpts:
9✔
862
            if np.allclose(np.array(x), xeq):
9✔
863
                seen = True
9✔
864
        if seen:
9✔
865
            continue
9✔
866

867
        # Save a new point
868
        equilpts += [xeq.tolist()]
9✔
869

870
    return equilpts
9✔
871

872

873
def _make_points(pointdata, gridspec, gridtype):
9✔
874
    # Check to see what type of data we got
875
    if isinstance(pointdata, np.ndarray) and gridtype is None:
9✔
876
        pointdata = np.atleast_2d(pointdata)
9✔
877
        if pointdata.shape[1] == 2:
9✔
878
            # Given a list of points => no action required
879
            return pointdata, None
9✔
880

881
    # Utility function to parse (and check) input arguments
882
    def _parse_args(defsize):
9✔
883
        if gridspec is None:
9✔
884
            return defsize
9✔
885

886
        elif not isinstance(gridspec, (list, tuple)) or \
9✔
887
             len(gridspec) != len(defsize):
888
            raise ValueError("invalid grid specification")
9✔
889

890
        return gridspec
9✔
891

892
    # Generate points based on grid type
893
    match gridtype:
9✔
894
        case 'boxgrid' | None:
9✔
895
            gridspec = _parse_args([6, 4])
9✔
896
            points = boxgrid(
9✔
897
                np.linspace(pointdata[0], pointdata[1], gridspec[0]),
898
                np.linspace(pointdata[2], pointdata[3], gridspec[1]))
899

900
        case 'meshgrid':
9✔
901
            gridspec = _parse_args([9, 6])
9✔
902
            points = meshgrid(
9✔
903
                np.linspace(pointdata[0], pointdata[1], gridspec[0]),
904
                np.linspace(pointdata[2], pointdata[3], gridspec[1]))
905

906
        case 'circlegrid':
9✔
907
            gridspec = _parse_args((0.5, 10))
9✔
908
            if isinstance(pointdata, np.ndarray):
9✔
909
                # Create circles around each point
910
                points = circlegrid(pointdata, gridspec[0], gridspec[1])
9✔
911
            else:
912
                # Create circle around center of the plot
913
                points = circlegrid(
9✔
914
                    np.array(
915
                        [(pointdata[0] + pointdata[1]) / 2,
916
                         (pointdata[0] + pointdata[1]) / 2]),
917
                    gridspec[0], gridspec[1])
918

919
        case _:
9✔
920
            raise ValueError(f"unknown grid type '{gridtype}'")
9✔
921

922
    return points, gridspec
9✔
923

924

925
def _parse_arrow_keywords(kwargs):
9✔
926
    # Get values for params (and pop from list to allow keyword use in plot)
927
    # TODO: turn this into a utility function (shared with nyquist_plot?)
928
    arrows = config._get_param(
9✔
929
        'phaseplot', 'arrows', kwargs, None, pop=True)
930
    arrow_size = config._get_param(
9✔
931
        'phaseplot', 'arrow_size', kwargs, None, pop=True)
932
    arrow_style = config._get_param('phaseplot', 'arrow_style', kwargs, None)
9✔
933

934
    # Parse the arrows keyword
935
    if not arrows:
9✔
936
        arrow_pos = []
×
937
    elif isinstance(arrows, int):
9✔
938
        N = arrows
9✔
939
        # Space arrows out, starting midway along each "region"
940
        arrow_pos = np.linspace(0.5/N, 1 + 0.5/N, N, endpoint=False)
9✔
941
    elif isinstance(arrows, (list, np.ndarray)):
×
942
        arrow_pos = np.sort(np.atleast_1d(arrows))
×
943
    else:
944
        raise ValueError("unknown or unsupported arrow location")
×
945

946
    # Set the arrow style
947
    if arrow_style is None:
9✔
948
        arrow_style = mpl.patches.ArrowStyle(
9✔
949
            'simple', head_width=int(2 * arrow_size / 3),
950
            head_length=arrow_size)
951

952
    return arrow_pos, arrow_style
9✔
953

954

955
# TODO: move to ctrlplot?
956
def _create_trajectory(
9✔
957
        sys, revsys, timepts, X0, params, dir, suppress_warnings=False,
958
        gridtype=None, gridspec=None, xlim=None, ylim=None):
959
    # Comput ethe forward trajectory
960
    if dir == 'forward' or dir == 'both':
9✔
961
        fwdresp = input_output_response(
9✔
962
            sys, timepts, X0=X0, params=params, ignore_errors=True)
963
        if not fwdresp.success and not suppress_warnings:
9✔
964
            warnings.warn(f"{X0=}, {fwdresp.message}")
9✔
965

966
    # Compute the reverse trajectory
967
    if dir == 'reverse' or dir == 'both':
9✔
968
        revresp = input_output_response(
9✔
969
            revsys, timepts, X0=X0, params=params, ignore_errors=True)
970
        if not revresp.success and not suppress_warnings:
9✔
971
            warnings.warn(f"{X0=}, {revresp.message}")
×
972

973
    # Create the trace to plot
974
    if dir == 'forward':
9✔
975
        traj = fwdresp.states
9✔
976
    elif dir == 'reverse':
9✔
977
        traj = revresp.states[:, ::-1]
9✔
978
    elif dir == 'both':
9✔
979
        traj = np.hstack([revresp.states[:, :1:-1], fwdresp.states])
9✔
980

981
    # Remove points outside the window (keep first point beyond boundary)
982
    inrange = np.asarray(
9✔
983
        (traj[0] >= xlim[0]) & (traj[0] <= xlim[1]) &
984
        (traj[1] >= ylim[0]) & (traj[1] <= ylim[1]))
985
    inrange[:-1] = inrange[:-1] | inrange[1:]   # keep if next point in range
9✔
986
    inrange[1:] = inrange[1:] | inrange[:-1]    # keep if prev point in range
9✔
987

988
    return traj[:, inrange]
9✔
989

990

991
def _make_timepts(timepts, i):
9✔
992
    if timepts is None:
9✔
993
        return np.linspace(0, 1)
9✔
994
    elif isinstance(timepts, (int, float)):
9✔
995
        return np.linspace(0, timepts)
9✔
996
    elif timepts.ndim == 2:
×
997
        return timepts[i]
×
998
    return timepts
×
999

1000

1001
#
1002
# Legacy phase plot function
1003
#
1004
# Author: Richard Murray
1005
# Date: 24 July 2011, converted from MATLAB version (2002); based on
1006
# a version by Kristi Morgansen
1007
#
1008
def phase_plot(odefun, X=None, Y=None, scale=1, X0=None, T=None,
9✔
1009
               lingrid=None, lintime=None, logtime=None, timepts=None,
1010
               parms=None, params=(), tfirst=False, verbose=True):
1011

1012
    """(legacy) Phase plot for 2D dynamical systems.
1013

1014
    .. deprecated:: 0.10.1
1015
        This function is deprecated; use :func:`phase_plane_plot` instead.
1016

1017
    Produces a vector field or stream line plot for a planar system.  This
1018
    function has been replaced by the :func:`~control.phase_plane_map` and
1019
    :func:`~control.phase_plane_plot` functions.
1020

1021
    Call signatures:
1022
      phase_plot(func, X, Y, ...) - display vector field on meshgrid
1023
      phase_plot(func, X, Y, scale, ...) - scale arrows
1024
      phase_plot(func. X0=(...), T=Tmax, ...) - display stream lines
1025
      phase_plot(func, X, Y, X0=[...], T=Tmax, ...) - plot both
1026
      phase_plot(func, X0=[...], T=Tmax, lingrid=N, ...) - plot both
1027
      phase_plot(func, X0=[...], lintime=N, ...) - stream lines with arrows
1028

1029
    Parameters
1030
    ----------
1031
    func : callable(x, t, ...)
1032
        Computes the time derivative of y (compatible with odeint).  The
1033
        function should be the same for as used for :mod:`scipy.integrate`.
1034
        Namely, it should be a function of the form dxdt = F(t, x) that
1035
        accepts a state x of dimension 2 and returns a derivative dx/dt of
1036
        dimension 2.
1037
    X, Y: 3-element sequences, optional, as [start, stop, npts]
1038
        Two 3-element sequences specifying x and y coordinates of a
1039
        grid.  These arguments are passed to linspace and meshgrid to
1040
        generate the points at which the vector field is plotted.  If
1041
        absent (or None), the vector field is not plotted.
1042
    scale: float, optional
1043
        Scale size of arrows; default = 1
1044
    X0: ndarray of initial conditions, optional
1045
        List of initial conditions from which streamlines are plotted.
1046
        Each initial condition should be a pair of numbers.
1047
    T: array-like or number, optional
1048
        Length of time to run simulations that generate streamlines.
1049
        If a single number, the same simulation time is used for all
1050
        initial conditions.  Otherwise, should be a list of length
1051
        len(X0) that gives the simulation time for each initial
1052
        condition.  Default value = 50.
1053
    lingrid : integer or 2-tuple of integers, optional
1054
        Argument is either N or (N, M).  If X0 is given and X, Y are missing,
1055
        a grid of arrows is produced using the limits of the initial
1056
        conditions, with N grid points in each dimension or N grid points in x
1057
        and M grid points in y.
1058
    lintime : integer or tuple (integer, float), optional
1059
        If a single integer N is given, draw N arrows using equally space time
1060
        points.  If a tuple (N, lambda) is given, draw N arrows using
1061
        exponential time constant lambda
1062
    timepts : array-like, optional
1063
        Draw arrows at the given list times [t1, t2, ...]
1064
    tfirst : bool, optional
1065
        If True, call `func` with signature `func(t, x, ...)`.
1066
    params: tuple, optional
1067
        List of parameters to pass to vector field: `func(x, t, *params)`
1068

1069
    See also
1070
    --------
1071
    box_grid : construct box-shaped grid of initial conditions
1072

1073
    """
1074
    # Generate a deprecation warning
1075
    warnings.warn(
9✔
1076
        "phase_plot() is deprecated; use phase_plane_plot() instead",
1077
        FutureWarning)
1078

1079
    #
1080
    # Figure out ranges for phase plot (argument processing)
1081
    #
1082
    #! TODO: need to add error checking to arguments
1083
    #! TODO: think through proper action if multiple options are given
1084
    #
1085
    autoFlag = False
9✔
1086
    logtimeFlag = False
9✔
1087
    timeptsFlag = False
9✔
1088
    Narrows = 0
9✔
1089

1090
    # Get parameters to pass to function
1091
    if parms:
9✔
1092
        warnings.warn(
9✔
1093
            "keyword 'parms' is deprecated; use 'params'", FutureWarning)
1094
        if params:
9✔
1095
            raise ControlArgument("duplicate keywords 'parms' and 'params'")
×
1096
        else:
1097
            params = parms
9✔
1098

1099
    if lingrid is not None:
9✔
1100
        autoFlag = True
9✔
1101
        Narrows = lingrid
9✔
1102
        if (verbose):
9✔
1103
            print('Using auto arrows\n')
×
1104

1105
    elif logtime is not None:
9✔
1106
        logtimeFlag = True
9✔
1107
        Narrows = logtime[0]
9✔
1108
        timefactor = logtime[1]
9✔
1109
        if (verbose):
9✔
1110
            print('Using logtime arrows\n')
×
1111

1112
    elif timepts is not None:
9✔
1113
        timeptsFlag = True
9✔
1114
        Narrows = len(timepts)
9✔
1115

1116
    # Figure out the set of points for the quiver plot
1117
    #! TODO: Add sanity checks
1118
    elif X is not None and Y is not None:
9✔
1119
        x1, x2 = np.meshgrid(
9✔
1120
            np.linspace(X[0], X[1], X[2]),
1121
            np.linspace(Y[0], Y[1], Y[2]))
1122
        Narrows = len(x1)
9✔
1123

1124
    else:
1125
        # If we weren't given any grid points, don't plot arrows
1126
        Narrows = 0
9✔
1127

1128
    if not autoFlag and not logtimeFlag and not timeptsFlag and Narrows > 0:
9✔
1129
        # Now calculate the vector field at those points
1130
        (nr,nc) = x1.shape
9✔
1131
        dx = np.empty((nr, nc, 2))
9✔
1132
        for i in range(nr):
9✔
1133
            for j in range(nc):
9✔
1134
                if tfirst:
9✔
1135
                    dx[i, j, :] = np.squeeze(
×
1136
                        odefun(0, [x1[i,j], x2[i,j]], *params))
1137
                else:
1138
                    dx[i, j, :] = np.squeeze(
9✔
1139
                        odefun([x1[i,j], x2[i,j]], 0, *params))
1140

1141
        # Plot the quiver plot
1142
        #! TODO: figure out arguments to make arrows show up correctly
1143
        if scale is None:
9✔
1144
            plt.quiver(x1, x2, dx[:,:,1], dx[:,:,2], angles='xy')
×
1145
        elif (scale != 0):
9✔
1146
            plt.quiver(x1, x2, dx[:,:,0]*np.abs(scale),
9✔
1147
                       dx[:,:,1]*np.abs(scale), angles='xy')
1148
            #! TODO: optimize parameters for arrows
1149
            #! TODO: figure out arguments to make arrows show up correctly
1150
            # xy = plt.quiver(...)
1151
            # set(xy, 'LineWidth', PP_arrow_linewidth, 'Color', 'b')
1152

1153
        #! TODO: Tweak the shape of the plot
1154
        # a=gca; set(a,'DataAspectRatio',[1,1,1])
1155
        # set(a,'XLim',X(1:2)); set(a,'YLim',Y(1:2))
1156
        plt.xlabel('x1'); plt.ylabel('x2')
9✔
1157

1158
    # See if we should also generate the streamlines
1159
    if X0 is None or len(X0) == 0:
9✔
1160
        return
9✔
1161

1162
    # Convert initial conditions to a numpy array
1163
    X0 = np.array(X0)
9✔
1164
    (nr, nc) = np.shape(X0)
9✔
1165

1166
    # Generate some empty matrices to keep arrow information
1167
    x1 = np.empty((nr, Narrows))
9✔
1168
    x2 = np.empty((nr, Narrows))
9✔
1169
    dx = np.empty((nr, Narrows, 2))
9✔
1170

1171
    # See if we were passed a simulation time
1172
    if T is None:
9✔
1173
        T = 50
9✔
1174

1175
    # Parse the time we were passed
1176
    TSPAN = T
9✔
1177
    if isinstance(T, (int, float)):
9✔
1178
        TSPAN = np.linspace(0, T, 100)
9✔
1179

1180
    # Figure out the limits for the plot
1181
    if scale is None:
9✔
1182
        # Assume that the current axis are set as we want them
1183
        alim = plt.axis()
×
1184
        xmin = alim[0]; xmax = alim[1]
×
1185
        ymin = alim[2]; ymax = alim[3]
×
1186
    else:
1187
        # Use the maximum extent of all trajectories
1188
        xmin = np.min(X0[:,0]); xmax = np.max(X0[:,0])
9✔
1189
        ymin = np.min(X0[:,1]); ymax = np.max(X0[:,1])
9✔
1190

1191
    # Generate the streamlines for each initial condition
1192
    for i in range(nr):
9✔
1193
        state = odeint(odefun, X0[i], TSPAN, args=params, tfirst=tfirst)
9✔
1194
        time = TSPAN
9✔
1195

1196
        plt.plot(state[:,0], state[:,1])
9✔
1197
        #! TODO: add back in colors for stream lines
1198
        # PP_stream_color(np.mod(i-1, len(PP_stream_color))+1))
1199
        # set(h[i], 'LineWidth', PP_stream_linewidth)
1200

1201
        # Plot arrows if quiver parameters were 'auto'
1202
        if autoFlag or logtimeFlag or timeptsFlag:
9✔
1203
            # Compute the locations of the arrows
1204
            #! TODO: check this logic to make sure it works in python
1205
            for j in range(Narrows):
9✔
1206

1207
                # Figure out starting index; headless arrows start at 0
1208
                k = -1 if scale is None else 0
9✔
1209

1210
                # Figure out what time index to use for the next point
1211
                if autoFlag:
9✔
1212
                    # Use a linear scaling based on ODE time vector
1213
                    tind = np.floor((len(time)/Narrows) * (j-k)) + k
×
1214
                elif logtimeFlag:
9✔
1215
                    # Use an exponential time vector
1216
                    # MATLAB: tind = find(time < (j-k) / lambda, 1, 'last')
1217
                    tarr = _find(time < (j-k) / timefactor)
9✔
1218
                    tind = tarr[-1] if len(tarr) else 0
9✔
1219
                elif timeptsFlag:
9✔
1220
                    # Use specified time points
1221
                    # MATLAB: tind = find(time < Y[j], 1, 'last')
1222
                    tarr = _find(time < timepts[j])
9✔
1223
                    tind = tarr[-1] if len(tarr) else 0
9✔
1224

1225
                # For tailless arrows, skip the first point
1226
                if tind == 0 and scale is None:
9✔
1227
                    continue
×
1228

1229
                # Figure out the arrow at this point on the curve
1230
                x1[i,j] = state[tind, 0]
9✔
1231
                x2[i,j] = state[tind, 1]
9✔
1232

1233
                # Skip arrows outside of initial condition box
1234
                if (scale is not None or
9✔
1235
                     (x1[i,j] <= xmax and x1[i,j] >= xmin and
1236
                      x2[i,j] <= ymax and x2[i,j] >= ymin)):
1237
                    if tfirst:
9✔
1238
                        pass
×
1239
                        v = odefun(0, [x1[i,j], x2[i,j]], *params)
×
1240
                    else:
1241
                        v = odefun([x1[i,j], x2[i,j]], 0, *params)
9✔
1242
                    dx[i, j, 0] = v[0]; dx[i, j, 1] = v[1]
9✔
1243
                else:
1244
                    dx[i, j, 0] = 0; dx[i, j, 1] = 0
×
1245

1246
    # Set the plot shape before plotting arrows to avoid warping
1247
    # a=gca
1248
    # if (scale != None):
1249
    #     set(a,'DataAspectRatio', [1,1,1])
1250
    # if (xmin != xmax and ymin != ymax):
1251
    #     plt.axis([xmin, xmax, ymin, ymax])
1252
    # set(a, 'Box', 'on')
1253

1254
    # Plot arrows on the streamlines
1255
    if scale is None and Narrows > 0:
9✔
1256
        # Use a tailless arrow
1257
        #! TODO: figure out arguments to make arrows show up correctly
1258
        plt.quiver(x1, x2, dx[:,:,0], dx[:,:,1], angles='xy')
×
1259
    elif scale != 0 and Narrows > 0:
9✔
1260
        plt.quiver(x1, x2, dx[:,:,0]*abs(scale), dx[:,:,1]*abs(scale),
9✔
1261
                   angles='xy')
1262
        #! TODO: figure out arguments to make arrows show up correctly
1263
        # xy = plt.quiver(...)
1264
        # set(xy, 'LineWidth', PP_arrow_linewidth)
1265
        # set(xy, 'AutoScale', 'off')
1266
        # set(xy, 'AutoScaleFactor', 0)
1267

1268
    if scale < 0:
9✔
1269
        plt.plot(x1, x2, 'b.');        # add dots at base
×
1270
        # bp = plt.plot(...)
1271
        # set(bp, 'MarkerSize', PP_arrow_markersize)
1272

1273

1274
# Utility function for generating initial conditions around a box
1275
def box_grid(xlimp, ylimp):
9✔
1276
    """box_grid   generate list of points on edge of box
1277

1278
    .. deprecated:: 0.10.0
1279
        Use :func:`phaseplot.boxgrid` instead.
1280

1281
    list = box_grid([xmin xmax xnum], [ymin ymax ynum]) generates a
1282
    list of points that correspond to a uniform grid at the end of the
1283
    box defined by the corners [xmin ymin] and [xmax ymax].
1284

1285
    """
1286

1287
    # Generate a deprecation warning
1288
    warnings.warn(
×
1289
        "box_grid() is deprecated; use phaseplot.boxgrid() instead",
1290
        FutureWarning)
1291

1292
    return boxgrid(
×
1293
        np.linspace(xlimp[0], xlimp[1], xlimp[2]),
1294
        np.linspace(ylimp[0], ylimp[1], ylimp[2]))
1295

1296

1297
# TODO: rename to something more useful (or remove??)
1298
def _find(condition):
9✔
1299
    """Returns indices where ravel(a) is true.
1300
    Private implementation of deprecated matplotlib.mlab.find
1301
    """
1302
    return np.nonzero(np.ravel(condition))[0]
9✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc