Source code for spb.plot_functions.vectors

from spb.plot_functions.functions_2d import _set_labels
from spb.graphics import vector_field_2d, vector_field_3d, graphics
from spb.graphics.vectors import _split_vector
from spb.utils import _plot_sympify, _is_range, _unpack_args
from sympy import Tuple


def _preprocess(*args):
    """Loops over the arguments and build a list of arguments having the
    following form: [expr, *ranges, label].
    `expr` can be a vector, a matrix or a list/tuple/Tuple.
    """

    if not all([isinstance(a, (list, tuple, Tuple)) for a in args]):
        # In this case we received arguments in one of the following forms.
        # Here we wrapped them into a list, so that they can be further
        # processed:
        #   v               -> [v]
        #   v, range        -> [v, range]
        #   v1, v2, ..., range   -> [v1, v2, range]
        args = [args]

    if any([_is_range(a) for a in args]):
        args = [args]

    new_args = []
    for a in args:
        exprs, ranges, label, rendering_kw = _unpack_args(*a)
        if len(exprs) == 1:
            new_args.append([*exprs, *ranges, label])
        else:
            # this is the case where the user provided: v1, v2, ..., range
            # we use the same ranges for each expression
            for e in exprs:
                new_args.append([e, *ranges, None])
    return new_args


[docs] def plot_vector(*args, **kwargs): """ Plot a 2D or 3D vector field. By default, the aspect ratio of the plot is set to `aspect="equal"`. Typical usage examples are in the followings: - Plotting a 2D vector field: .. code-block:: plot_vector(vec, range_x, range_y, **kwargs) - Plotting multiple 2D vector fields with different ranges and custom labels: .. code-block:: plot_vector( (vec1, range1_x, range1_y, label1 [opt]), (vec2, range2_x, range2_y, label2 [opt]), **kwargs) - Plotting a 3D vector field: .. code-block:: plot_vector(vec, range_x, range_y, range_z, **kwargs) - Plotting multiple 3D vector fields with different ranges and custom labels: .. code-block:: plot_vector( (vec1, range1_x, range1_y, range1_z, label1 [opt]), (vec2, range2_x, range2_y, range2_z, label2 [opt]), **kwargs) Refer to :func:`~spb.graphics.vectors.vector_field_2d` for a full list of keyword arguments to customize the appearances of quivers, streamlines and contours for a 2D vector field. Refer to :func:`~spb.graphics.vectors.vector_field_3d` for a full list of keyword arguments to customize the appearances of quivers and streamlines for a 3D vector field. Refer to :func:`~spb.graphics.graphics.graphics` for a full list of keyword arguments to customize the appearances of the figure (title, axis labels, ...). Examples ======== .. plot:: :context: reset :format: doctest :include-source: True >>> from sympy import symbols, sin, cos, Plane, Matrix, sqrt, latex >>> from spb import plot_vector >>> x, y, z = symbols('x, y, z') Quivers plot of a 2D vector field with a contour plot in background representing the vector's magnitude (a scalar field). .. plot:: :context: close-figs :format: doctest :include-source: True >>> v = [sin(x - y), cos(x + y)] >>> plot_vector(v, (x, -3, 3), (y, -3, 3), ... quiver_kw=dict(color="black", scale=30, headwidth=5), ... contour_kw={"cmap": "Blues_r", "levels": 15}, ... grid=False, xlabel="x", ylabel="y") Plot object containing: [0]: contour: sqrt(sin(x - y)**2 + cos(x + y)**2) for x over (-3.0, 3.0) and y over (-3.0, 3.0) [1]: 2D vector series: [sin(x - y), cos(x + y)] over (x, -3.0, 3.0), (y, -3.0, 3.0) Quivers plot of a 2D vector field with no background scalar field, a custom label and normalized quiver lengths: .. plot:: :context: close-figs :format: doctest :include-source: True >>> plot_vector( ... v, (x, -3, 3), (y, -3, 3), ... label="Magnitude of $%s$" % latex([-sin(y), cos(x)]), ... scalar=False, normalize=True, ... quiver_kw={ ... "scale": 35, "headwidth": 4, "cmap": "gray", ... "clim": [0, 1.6]}, ... grid=False, xlabel="x", ylabel="y") Plot object containing: [0]: 2D vector series: [sin(x - y), cos(x + y)] over (x, -3.0, 3.0), (y, -3.0, 3.0) Streamlines plot of a 2D vector field with no background scalar field, and a custom label: .. plot:: :context: close-figs :format: doctest :include-source: True >>> plot_vector(v, (x, -3, 3), (y, -3, 3), ... streamlines=True, scalar=None, ... stream_kw={"density": 1.5}, ... label="Magnitude of %s" % str(v), xlabel="x", ylabel="y") Plot object containing: [0]: 2D vector series: [sin(x - y), cos(x + y)] over (x, -3.0, 3.0), (y, -3.0, 3.0) Plot multiple 2D vectors fields, setting a background scalar field to be the magnitude of the first vector. Also, apply custom rendering options to all data series. .. plot:: :context: close-figs :format: doctest :include-source: True >>> scalar_expr = sqrt((-sin(y))**2 + cos(x)**2) >>> plot_vector([-sin(y), cos(x)], [2 * y, x], (x, -5, 5), (y, -3, 3), ... n=20, legend=True, grid=False, xlabel="x", ylabel="y", ... scalar=[scalar_expr, "$%s$" % latex(scalar_expr)], use_cm=False, ... rendering_kw=[ ... {"cmap": "summer"}, # to the contour ... {"color": "k"}, # to the first quiver ... {"color": "r"} # to the second quiver ... ]) Plot object containing: [0]: contour: sqrt(sin(y)**2 + cos(x)**2) for x over (-5.0, 5.0) and y over (-3.0, 3.0) [1]: 2D vector series: [-sin(y), cos(x)] over (x, -5.0, 5.0), (y, -3.0, 3.0) [2]: 2D vector series: [2*y, x] over (x, -5.0, 5.0), (y, -3.0, 3.0) Plotting a the streamlines of a 2D vector field defined with numerical functions instead of symbolic expressions: .. plot:: :context: close-figs :format: doctest :include-source: True >>> import numpy as np >>> f = lambda x, y: np.sin(2 * x + 2 * y) >>> fx = lambda x, y: np.cos(f(x, y)) >>> fy = lambda x, y: np.sin(f(x, y)) >>> plot_vector([fx, fy], ("x", -1, 1), ("y", -1, 1), ... streamlines=True, scalar=False, use_cm=False) # doctest: +SKIP Interactive-widget 2D vector plot. Refer to the interactive sub-module documentation to learn more about the ``params`` dictionary. This plot illustrates: * customizing the appearance of quivers and countour. * the use of ``prange`` (parametric plotting range). * the use of the ``params`` dictionary to specify sliders in their basic form: (default, min, max). * the use of :py:class:`panel.widgets.slider.RangeSlider`, which is a 2-values widget. .. panel-screenshot:: :small-size: 800, 610 from sympy import * from spb import * import panel as pn x, y, a, b, c, d, e = symbols("x, y, a, b, c, d, e") v = [-sin(a * y), cos(b * x)] plot_vector( v, prange(x, -3*c, 3*c), prange(y, d, e), params={ a: (1, -2, 2), b: (1, -2, 2), c: (1, 0, 2), (d, e): pn.widgets.RangeSlider( value=(-3, 3), start=-9, end=9, step=0.1) }, quiver_kw=dict(color="black", scale=30, headwidth=5), contour_kw={"cmap": "Blues_r", "levels": 15}, grid=False, xlabel="x", ylabel="y", imodule="panel", servable=True ) 3D vector field. .. k3d-screenshot:: from sympy import * from spb import * var("x:z") plot_vector([z, y, x], (x, -10, 10), (y, -10, 10), (z, -10, 10), backend=KB, n=8, xlabel="x", ylabel="y", zlabel="z", quiver_kw={"scale": 0.5, "line_width": 0.1, "head_size": 10}) 3D vector field with 3 orthogonal slice planes. .. k3d-screenshot:: :camera: 18.45, -25.63, 14.10, 0.45, -1.02, -2.32, -0.25, 0.35, 0.9 from sympy import * from spb import * var("x:z") plot_vector([z, y, x], (x, -10, 10), (y, -10, 10), (z, -10, 10), backend=KB, n=8, use_cm=False, grid=False, xlabel="x", ylabel="y", zlabel="z", quiver_kw={"scale": 0.25, "line_width": 0.1, "head_size": 10}, slice=[ Plane((-10, 0, 0), (1, 0, 0)), Plane((0, 10, 0), (0, 2, 0)), Plane((0, 0, -10), (0, 0, 1))]) 3D vector streamlines starting at a 300 random points: .. k3d-screenshot:: :camera: 3.7, -8.16, 2.8, -0.75, -0.51, -0.63, -0.16, 0.27, 0.96 from sympy import * from spb import * import k3d var("x:z") plot_vector(Matrix([z, -x, y]), (x, -3, 3), (y, -3, 3), (z, -3, 3), backend=KB, n=40, streamlines=True, stream_kw=dict( starts=True, npoints=400, width=0.025, color_map=k3d.colormaps.matplotlib_color_maps.viridis ), xlabel="x", ylabel="y", zlabel="z") 3D vector streamlines starting at the XY plane. Note that the number of discretization points of the plane controls the numbers of streamlines. .. k3d-screenshot:: :camera: -2.64, -22.6, 8.8, 0.03, -0.6, -1.13, 0.1, 0.35, 0.93 from sympy import * from spb import * import k3d var("x:z") u = -y - z v = x + y / 5 w = S(1) / 5 + (x - S(5) / 2) * z s = 10 # length of the cubic discretization volume # create an XY plane with n discretization points along each direction n = 8 p1 = plot_geometry( Plane((0, 0, 0), (0, 0, 1)), (x, -s, s), (y, -s, s), (z, -s, s), n1=n, n2=n, show=False) # extract the coordinates of the starting points for the streamlines xx, yy, zz = p1[0].get_data() # streamlines plot plot_vector(Matrix([u, v, w]), (x, -s, s), (y, -s, s), (z, -s, s), backend=KB, n=40, streamlines=True, grid=False, stream_kw=dict( starts=dict(x=xx, y=yy, z=zz), width=0.025, color_map=k3d.colormaps.matplotlib_color_maps.plasma ), title=r"Rössler \, attractor", xlabel="x", ylabel="y", zlabel="z") Visually verify the normal vector to a circular cone surface. The following steps are executed: 1. compute the normal vector to a circular cone surface. This will be the vector field to be plotted. 2. plot the cone surface for visualization purposes (use high number of discretization points). 3. plot the cone surface that will be used to slice the vector field (use a low number of discretization points). The data series associated to this plot will be used in the ``slice`` keyword argument in the next step. 4. plot the sliced vector field. 5. combine the plots of step 4 and 2 to get a nice visualization. .. k3d-screenshot:: :camera: 4.5, -3.9, 2, 1.3, 0.04, -0.36, -0.25, 0.27, 0.93 from sympy import tan, cos, sin, pi, symbols from spb import plot3d_parametric_surface, plot_vector, KB from sympy.vector import CoordSys3D, gradient u, v = symbols("u, v") N = CoordSys3D("N") i, j, k = N.base_vectors() xn, yn, zn = N.base_scalars() t = 0.35 # half-cone angle in radians expr = -xn**2 * tan(t)**2 + yn**2 + zn**2 # cone surface equation g = gradient(expr) n = g / g.magnitude() # unit normal vector n1, n2 = 10, 20 # number of discretization points for the vector field # cone surface for visualization (high number of discretization points) p1 = plot3d_parametric_surface( u / tan(t), u * cos(v), u * sin(v), (u, 0, 1), (v, 0 , 2*pi), {"opacity": 1}, backend=KB, show=False, wireframe=True, wf_n1=n1, wf_n2=n2, wf_rendering_kw={"width": 0.004}) # cone surface to discretize vector field (low numb of discret points) p2 = plot3d_parametric_surface( u / tan(t), u * cos(v), u * sin(v), (u, 0, 1), (v, 0 , 2*pi), n1=n1, n2=n2, show=False) # plot vector field on over the surface of the cone p3 = plot_vector( n, (xn, -5, 5), (yn, -5, 5), (zn, -5, 5), slice=p2[0], backend=KB, use_cm=False, show=False, quiver_kw={"scale": 0.5, "pivot": "tail"}) (p1 + p3).show() """ args = _plot_sympify(args) args = _preprocess(*args) kwargs.setdefault("aspect", "equal") global_labels = kwargs.pop("label", []) global_rendering_kw = kwargs.pop("rendering_kw", None) scalar = kwargs.pop("scalar", -1) if (scalar == -1) and len(args) == 1: scalar = True series = [] for i, a in enumerate(args): vec = _split_vector(a[0]) label = None if not isinstance(a[-1], str) else a[-1] ranges = a[1:-1] kw = kwargs.copy() kw.update(dict(zip(["range1", "range2", "range3"], ranges))) kw["label"] = label kw["scalar"] = scalar if (i == 0) and (scalar != -1) else None if (vec[-1] is None) or (vec[-1] == 0): series.extend(vector_field_2d(*vec[:2], **kw)) else: series.extend(vector_field_3d(*vec, **kw)) _set_labels(series, global_labels, global_rendering_kw) return graphics(*series, **kwargs)