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

python-control / python-control / 11829967598

14 Nov 2024 03:06AM UTC coverage: 94.692% (-0.002%) from 94.694%
11829967598

push

github

web-flow
Merge pull request #1054 from murrayrm/find_operating_point-06Nov2024

Update find_eqpt to find_operating_point, adding root_method + better docs

9170 of 9684 relevant lines covered (94.69%)

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.append(
9✔
536
                ax.plot(xeq[0], xeq[1], marker='o', color=color))
537
    return out
9✔
538

539

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

704

705
#
706
# User accessible utility functions
707
#
708

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

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

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

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

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

736

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

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

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

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

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

762
    return grid
9✔
763

764

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

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

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

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

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

795
#
796
# Internal utility functions
797
#
798

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

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

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

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

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

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

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

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

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

849
    return xlim, ylim, maxlim
9✔
850

851

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

859
        if xeq is None:
9✔
860
            continue            # didn't find anything
9✔
861

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

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

873
    return equilpts
9✔
874

875

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

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

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

893
        return gridspec
9✔
894

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

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

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

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

925
    return points, gridspec
9✔
926

927

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

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

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

955
    return arrow_pos, arrow_style
9✔
956

957

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

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

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

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

991
    return traj[:, inrange]
9✔
992

993

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

1003

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

1015
    """(legacy) Phase plot for 2D dynamical systems.
1016

1017
    .. deprecated:: 0.10.1
1018
        This function is deprecated; use :func:`phase_plane_plot` instead.
1019

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

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

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

1072
    See also
1073
    --------
1074
    box_grid : construct box-shaped grid of initial conditions
1075

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1269
    if scale < 0:
9✔
1270
        bp = plt.plot(x1, x2, 'b.');        # add dots at base
×
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