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

python-control / python-control / 13085325229

01 Feb 2025 04:49AM UTC coverage: 94.659% (+0.01%) from 94.649%
13085325229

Pull #1108

github

web-flow
Merge e539c7295 into ebff1259a
Pull Request #1108: scipy-based implementation of ss2tf

9677 of 10223 relevant lines covered (94.66%)

8.27 hits per line

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

92.44
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 ControlNotImplemented
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
    passed_kwargs = False
9✔
166

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

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

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

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

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

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

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

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

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

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

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

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

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

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

240

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

331
    return out
9✔
332

333

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

454

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

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

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

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

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

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

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

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

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

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

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

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

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

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

538

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

702

703
#
704
# User accessible utility functions
705
#
706

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

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

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

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

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

734

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

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

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

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

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

760
    return grid
9✔
761

762

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

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

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

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

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

793
#
794
# Internal utility functions
795
#
796

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

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

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

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

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

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

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

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

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

847
    return xlim, ylim, maxlim
9✔
848

849

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

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

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

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

871
    return equilpts
9✔
872

873

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

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

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

891
        return gridspec
9✔
892

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

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

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

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

923
    return points, gridspec
9✔
924

925

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

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

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

953
    return arrow_pos, arrow_style
9✔
954

955

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

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

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

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

989
    return traj[:, inrange]
9✔
990

991

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

1001

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1142
        # Plot the quiver plot
1143
        #! TODO: figure out arguments to make arrows show up correctly
1144
        if scale is None:
9✔
1145
            plt.quiver(x1, x2, dx[:,:,1], dx[:,:,2], angles='xy')
×
1146
        elif (scale != 0):
9✔
1147
            #! TODO: optimize parameters for arrows
1148
            #! TODO: figure out arguments to make arrows show up correctly
1149
            xy = plt.quiver(x1, x2, dx[:,:,0]*np.abs(scale),
9✔
1150
                            dx[:,:,1]*np.abs(scale), angles='xy')
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
        #! TODO: figure out arguments to make arrows show up correctly
1261
        xy = plt.quiver(x1, x2, dx[:,:,0]*abs(scale), dx[:,:,1]*abs(scale),
9✔
1262
                        angles='xy')
1263
        # set(xy, 'LineWidth', PP_arrow_linewidth)
1264
        # set(xy, 'AutoScale', 'off')
1265
        # set(xy, 'AutoScaleFactor', 0)
1266

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

1271

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

1276
    .. deprecated:: 0.10.0
1277
        Use :func:`phaseplot.boxgrid` instead.
1278

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

1283
    """
1284

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

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

1294

1295
# TODO: rename to something more useful (or remove??)
1296
def _find(condition):
9✔
1297
    """Returns indices where ravel(a) is true.
1298
    Private implementation of deprecated matplotlib.mlab.find
1299
    """
1300
    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