Skip to content

PhasePortrait3D

PhasePortrait3D

PhasePortrait3D

Makes a phase portrait of a 3D system.

Examples

image

import numpy as np
import matplotlib.pyplot as plt
# Create an instance of PhasePortrait3D
from phaseportrait.PhasePortrait3D import PhasePortrait3D



# Define your differential equation function
# This is a different system than the above image
def dF(x, y, z, a=0.1, b=0.2, c=0.2):
    dx = a * (y - x)
    dy = x * (b - z) - y
    dz = x * y - c * z
    return dx, dy, dz


# Define the ranges for x, y, and z axes
Range = [[-10, 10], [-10, 10], [-10, 10]]

# Create a PhasePortrait3D object
phase_portrait = PhasePortrait3D(dF, Range)

# Plot the phase portrait
phase_portrait.plot()

# Add sliders for parameters if needed
# phase_portrait.add_slider('a', valinit=0.1, valstep=0.01, valinterval=[0.01, 0.5])
# phase_portrait.add_slider('b', valinit=0.2, valstep=0.01, valinterval=[0.01, 0.5])
# phase_portrait.add_slider('c', valinit=0.2, valstep=0.01, valinterval=[0.01, 0.5])

# Show the plot
plt.show()

Methods

  • draw_plot : Draws the streamplot. Is intenally used by method plot.
  • add_function : Adds a function to the dF plot.
  • add_slider : Adds a Slider for the dF function.
  • plot : Prepares the plots and computes the values. Returns the axis and the figure.
Source code in phaseportrait/PhasePortrait3D.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
class PhasePortrait3D:
    """
    PhasePortrait3D
    ----------------
    Makes a phase portrait of a 3D system.

    Examples
    -------
    ![image](../../imgs/doc_examples/pp3d_example.png)

    ```py
    import numpy as np
    import matplotlib.pyplot as plt
    # Create an instance of PhasePortrait3D
    from phaseportrait.PhasePortrait3D import PhasePortrait3D



    # Define your differential equation function
    # This is a different system than the above image
    def dF(x, y, z, a=0.1, b=0.2, c=0.2):
        dx = a * (y - x)
        dy = x * (b - z) - y
        dz = x * y - c * z
        return dx, dy, dz


    # Define the ranges for x, y, and z axes
    Range = [[-10, 10], [-10, 10], [-10, 10]]

    # Create a PhasePortrait3D object
    phase_portrait = PhasePortrait3D(dF, Range)

    # Plot the phase portrait
    phase_portrait.plot()

    # Add sliders for parameters if needed
    # phase_portrait.add_slider('a', valinit=0.1, valstep=0.01, valinterval=[0.01, 0.5])
    # phase_portrait.add_slider('b', valinit=0.2, valstep=0.01, valinterval=[0.01, 0.5])
    # phase_portrait.add_slider('c', valinit=0.2, valstep=0.01, valinterval=[0.01, 0.5])

    # Show the plot
    plt.show()
    ```

    Methods 
    -----
    * draw_plot : Draws the streamplot. Is intenally used by method `plot`.
    * add_function : Adds a function to the `dF` plot.
    * add_slider : Adds a `Slider` for the `dF` function.
    * plot : Prepares the plots and computes the values. Returns the axis and the figure.
    """
    _name_ = 'PhasePortrait3D'
    def __init__(self, dF, Range, *, MeshDim=6, dF_args={}, Density = 1, Polar = False, Title = 'Phase Portrait', xlabel = 'X', ylabel = 'Y', 
                 zlabel='Z', color='rainbow', xScale='linear', yScale='linear', zScale='linear', maxLen=500, odeint_method="scipy", **kargs):
        """ PhasePortrait3D

        Args:
            dF (callable): A dF type function.
            Range ([x_range, y_range, z_range]): Ranges of the axis in the main plot.
            MeshDim (int, default=30): Number of elements in the arrows grid.
            dF_args (dict): If necesary, must contain the kargs for the `dF` function.
            Density (float, default=1): [Deprecated] Number of elements in the arrows grid plot.
            Polar (bool, default=False): Whether to use polar coordinates or not.
            Title (str, default='Phase Portrait' ): xlabel (str, default='X'): x label of the plot.
            ylabel (str, default='Y' ): y label of the plot.
            zlabel (str, default='Z' ): z label of the plot.
            color (str, default='rainbow'): Matplotlib `Cmap`.
            xScale (str, default='linear'): x axis scale. Can be `linear`, `log`, `symlog`, `logit`.
            yScale (str, default='linear'): y axis scale. Can be `linear`, `log`, `symlog`, `logit`.
            zScale (str, default='linear'): z axis scale. Can be `linear`, `log`, `symlog`, `logit`.
            odeint_method (str, default="scipy"): Selects integration method, by default uses scipy.odeint. `euler` and `rungekutta3` are also available.
        """
        self.sliders = {}
        self.nullclines = []

        self.dF_args = dF_args.copy()                    # dF function's args
        self.dF = dF                                     # Function containing system's equations


        self.MeshDim  = MeshDim
        self.Density = Density                           # Controls concentration of nearby trajectories
        self.Polar = Polar                               # If dF expression given in polar coord. mark as True
        self.Title = Title                               # Title of the plot
        self.xlabel = xlabel                             # Title on X axis
        self.ylabel = ylabel                             # Title on Y axis
        self.zlabel = zlabel

        self.xScale = xScale                             # x axis scale
        self.yScale = yScale                             # y axis scale
        self.zScale = zScale                             # z axis scale
        self.Range = Range                               # Range of graphical representation

        self.streamplot_callback = Streamlines_Velocity_Color_Gradient

        self._create_arrays()

        # Variables for plotting
        self.fig = kargs.get('fig', None)
        if self.fig:
            self.fig.gca().remove()
        else:
            self.fig = plt.figure()

        self.ax = self.fig.add_subplot(projection='3d')
        self.color = color
        self.grid = True

        self.streamplot_args = {"maxLen": maxLen, "odeint_method": odeint_method}


        self.manager = manager.Manager(self)


    def _create_arrays(self):
        """
        Helper method to create arrays for plotting based on the specified ranges and scales.
        """
        # If scale is log and min range value is 0 or negative, adjust the range to avoid errors in plotting
        _Range = self.Range.copy()
        for i, (scale, Range) in enumerate(zip([self.xScale, self.yScale, self.zScale], self.Range)):
            if scale == 'log':
                for j in range(len(Range)):
                    if Range[j] <= 0:
                        # If range value is 0 or negative, set it to a small positive value for log scale
                        _Range[i, j] = abs(max(Range)) / 100 if j == 0 else abs(max(Range))
        self.Range = _Range

        # Create arrays based on the specified scales
        for i, (_P, scale, Range) in enumerate(zip(["_X", "_Y", "_Z"], [self.xScale, self.yScale, self.zScale], self.Range)):
            if scale == 'linear':
                # If scale is linear, create linearly spaced array within the range
                setattr(self, _P, np.linspace(Range[0], Range[1], self.MeshDim))
            if scale == 'log':
                # If scale is logarithmic, create log-spaced array within the range
                setattr(self, _P, np.logspace(np.log10(Range[0]), np.log10(Range[1]), self.MeshDim))
            if scale == 'symlog':
                # If scale is symmetrical log, create linearly spaced array within the range
                setattr(self, _P, np.linspace(Range[0], Range[1], self.MeshDim))

        # Create meshgrid for the arrays
        self._X, self._Y, self._Z = np.meshgrid(self._X, self._Y, self._Z)

        # If polar coordinates are specified, convert Cartesian coordinates to polar coordinates
        if self.Polar:
            self._R, self._Theta = (self._X ** 2 + self._Y ** 2) ** 0.5, np.arctan2(self._Y, self._X)



    def plot(self, *, color=None, grid=None):
        """
        Prepares the plots and computes the values.

        Args:
            color (str): Matplotlib colormap.
            grid (bool): Show grid lines.

        Returns:
            (tuple(matplotlib Figure, matplotlib Axis)): Tuple containing the figure and axis objects.
        """
        # Update plot color and grid settings if specified
        if color is not None:
            self.color = color
        if grid is not None:
            self.grid = grid

        # Draw the plot using the specified color and grid settings
        self.stream = self.draw_plot(color=self.color, grid=grid)

        # Add colorbar if the plot stream has velocity normalization
        if hasattr(self, "colorbar_ax"):
            # Create a colorbar using ScalarMappable with velocity normalization and specified colormap
            cb = plt.colorbar(mplcm.ScalarMappable(
                norm=self.stream._velocity_normalization(), 
                cmap=self.color),
                ax=self.ax,
                cax=self.colorbar_ax)

            # Update colorbar axis attribute
            self.colorbar_ax = cb.ax

        # Redraw the canvas to reflect any changes
        self.fig.canvas.draw_idle()

        # Return the figure and axis objects
        return self.fig, self.ax


    def colorbar(self, toggle=True):
        """
        Adds a colorbar for speed.

        Args:
            toggle (bool, default=True): If `True` colorbar is visible.
        """
        if (not hasattr(self, "colorbar_ax")) and toggle:
            self.colorbar_ax = None
        else:
            if hasattr(self, "colorbar_ax"):
                self.colorbar_ax.remove()
                del(self.colorbar_ax)


    def draw_plot(self, *, color=None, grid=None):
        """
        Draws the streamplot. Is intenally used by method `plot`.

        Args:
            color (str, default='viridis'): Matplotlib `Cmap`.
            grid (bool, default=True): Show grid lines.

        Returns:
            (matplotlib.Streamplot):
        """
        self.dF_args.update({name: slider.value for name, slider in self.sliders.items() if slider.value!= None})

        self._create_arrays()

        try:
            for nullcline in self.nullclines:
                nullcline.plot()
        except AttributeError:
            pass

        if color is not None:
            self.color = color

        # if self.Polar:
        #     self._PolarTransformation()
        # else:
        #     self._dX, self._dY = self.dF(self._X, self._Y, **self.dF_args)

        # if utils.is_number(self._dX):
        #     self._dX = self._X.copy() * 0 + self._dX
        # if utils.is_number(self._dY):
        #     self._dY = self._Y.copy() * 0 + self._dY



        stream = self.streamplot_callback(self.dF, self._X, self._Y, self._Z,
            dF_args=self.dF_args, polar=self.Polar, **self.streamplot_args)

        try:
            norm = stream._velocity_normalization()
        except AttributeError:
            norm = None
        cmap = plt.get_cmap(self.color)

        stream.plot(self.ax, cmap, norm, arrowsize=self.streamplot_args.get('arrow_width', 1))


        self.ax.set_xlim3d(self.Range[0])
        self.ax.set_ylim3d(self.Range[1])
        self.ax.set_zlim3d(self.Range[2])
        # self.ax.set_aspect(abs(self.Range[0,1]-self.Range[0,0])/abs(self.Range[1,1]-self.Range[1,0]))

        self.ax.set_title(f'{self.Title}')
        self.ax.set_xlabel(f'{self.xlabel}')
        self.ax.set_ylabel(f'{self.ylabel}')
        self.ax.set_zlabel(f'{self.zlabel}')
        self.ax.set_xscale(self.xScale)
        self.ax.set_yscale(self.yScale)
        self.ax.set_zscale(self.zScale)




        def log_tick_formatter(val, pos=None):
            return r"$10^{{{:.0f}}}$".format(val)

        if self.xScale in ['log', 'symlog']:
            self.ax.xaxis.set_major_formatter(mticker.FuncFormatter(log_tick_formatter))
        if self.yScale in ['log', 'symlog']:
            self.ax.yaxis.set_major_formatter(mticker.FuncFormatter(log_tick_formatter))
        if self.zScale in ['log', 'symlog']:
            self.ax.zaxis.set_major_formatter(mticker.FuncFormatter(log_tick_formatter))

        self.ax.grid(grid if grid is not None else self.grid)

        return stream


    def add_slider(self, param_name, *, valinit=None, valstep=0.1, valinterval=10):
        """
        Adds a slider which can change the value of a parameter in execution time.

        Args:
            param_name (str): It takes the name of the parameter on which the slider will be defined. Must be the same as the one appearing as karg in the `dF` function.
            valinit (float, optional): Initial value of *param_name* variable. Default value is 0.5.
            valstep (float, optional): Slider step value. Default value is 0.1.
            valinterval (float|list[float], optional): Slider range. Default value is [-10, 10].
        """

        self.sliders.update({param_name: sliders.Slider(self, param_name, valinit=valinit, valstep=valstep, valinterval=valinterval)})

        self.fig.subplots_adjust(bottom=0.25)

        self.sliders[param_name].slider.on_changed(self.sliders[param_name])

    def _PolarTransformation(self):
        """
        Computes the expression of the velocity field if coordinates are given in polar representation.

        The transformation equations are as follows:

        R = sqrt(X^2 + Y^2 + Z^2)
        Theta = arctan2(Y, X)
        Phi = arccos(Z / R)

        where:
        - R is the radial distance from the origin,
        - Theta is the azimuthal angle measured from the positive x-axis,
        - Phi is the polar angle measured from the positive z-axis.

        The derivatives of R, Theta, and Phi with respect to time are computed using the provided dF function.
        Then, the derivatives of the Cartesian coordinates (X, Y, Z) with respect to time are calculated using the chain rule:

        dX/dt = dR/dt * cos(Theta) * sin(Phi) - R * sin(Theta) * sin(Phi) * dTheta/dt + R * cos(Theta) * cos(Phi) * dPhi/dt
        dY/dt = dR/dt * sin(Theta) * sin(Phi) + R * cos(Theta) * sin(Phi) * dTheta/dt + R * sin(Theta) * cos(Phi) * dPhi/dt
        dZ/dt = dR/dt * cos(Phi) - R * sin(Phi) * dPhi/dt

        These equations represent the velocity components in the Cartesian coordinate system.
        """
        if not hasattr(self, "_dR") or not hasattr(self, "_dTheta") or not hasattr(self, "_dPhi"):
            self._R = np.sqrt(self._X**2 + self._Y**2 + self._Z**2)
            self._Theta = np.arctan2(self._Y, self._X)
            self._Phi = np.arccos(self._Z / self._R)

        self._dR, self._dTheta, self._dPhi = self.dF(self._R, self._Theta, self._Phi ** self.dF_args)

        self._dX = self._dR * np.cos(self._Theta) * np.sin(self._Phi) - self._R * np.sin(self._Theta) * np.sin(self._Phi) * self._dTheta + self._R * np.cos(self._Theta) * np.cos(self._Phi) * self._dPhi
        self._dY = self._dR * np.sin(self._Theta) * np.sin(self._Phi) + self._R * np.cos(self._Theta) * np.sin(self._Phi) * self._dTheta + self._R * np.sin(self._Theta) * np.cos(self._Phi) * self._dPhi
        self._dZ = self._dR * np.cos(self._Phi) - self._R * np.sin(self._Phi) * self._dPhi



    @property
    def dF(self):
        return self._dF

    @dF.setter
    def dF(self, func):
        if not callable(func):
            raise exceptions.dFNotCallable(func)
        sig = signature(func)
        if len(sig.parameters)<2 + len(self.dF_args):
            raise exceptions.dFInvalid(sig, self.dF_args)

        # TODO: when a slider is created it should create and append an axis to the figure. For easier cleaning
        for s in self.sliders.copy():
            if s not in sig.parameters:
                raise exceptions.dF_argsInvalid(self.dF_args)

        self._dF = func

    @property
    def Range(self):
        return self._Range


    @Range.setter
    def Range(self, value):
        if len(value)==3:
            if np.array([len(value[i])==2 for i in range(3)]).all():
                self._Range = value
                return
        self._Range = np.array(utils.construct_interval(value, dim=3))
        self._create_arrays()

    @property
    def dF_args(self):
        return self._dF_args

    @dF_args.setter
    def dF_args(self, value):
        if value:
            if not isinstance(value, dict):
                raise exceptions.dF_argsInvalid(value)
        self._dF_args = value

__init__(dF, Range, *, MeshDim=6, dF_args={}, Density=1, Polar=False, Title='Phase Portrait', xlabel='X', ylabel='Y', zlabel='Z', color='rainbow', xScale='linear', yScale='linear', zScale='linear', maxLen=500, odeint_method='scipy', **kargs)

PhasePortrait3D

Parameters:

Name Type Description Default
dF callable

A dF type function.

required
Range [x_range, y_range, z_range]

Ranges of the axis in the main plot.

required
MeshDim int, default=30

Number of elements in the arrows grid.

6
dF_args dict

If necesary, must contain the kargs for the dF function.

{}
Density float, default=1

[Deprecated] Number of elements in the arrows grid plot.

1
Polar bool, default=False

Whether to use polar coordinates or not.

False
Title str, default='Phase Portrait'

xlabel (str, default='X'): x label of the plot.

'Phase Portrait'
ylabel str, default='Y'

y label of the plot.

'Y'
zlabel str, default='Z'

z label of the plot.

'Z'
color str, default='rainbow'

Matplotlib Cmap.

'rainbow'
xScale str, default='linear'

x axis scale. Can be linear, log, symlog, logit.

'linear'
yScale str, default='linear'

y axis scale. Can be linear, log, symlog, logit.

'linear'
zScale str, default='linear'

z axis scale. Can be linear, log, symlog, logit.

'linear'
odeint_method str, default="scipy"

Selects integration method, by default uses scipy.odeint. euler and rungekutta3 are also available.

'scipy'
Source code in phaseportrait/PhasePortrait3D.py
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
def __init__(self, dF, Range, *, MeshDim=6, dF_args={}, Density = 1, Polar = False, Title = 'Phase Portrait', xlabel = 'X', ylabel = 'Y', 
             zlabel='Z', color='rainbow', xScale='linear', yScale='linear', zScale='linear', maxLen=500, odeint_method="scipy", **kargs):
    """ PhasePortrait3D

    Args:
        dF (callable): A dF type function.
        Range ([x_range, y_range, z_range]): Ranges of the axis in the main plot.
        MeshDim (int, default=30): Number of elements in the arrows grid.
        dF_args (dict): If necesary, must contain the kargs for the `dF` function.
        Density (float, default=1): [Deprecated] Number of elements in the arrows grid plot.
        Polar (bool, default=False): Whether to use polar coordinates or not.
        Title (str, default='Phase Portrait' ): xlabel (str, default='X'): x label of the plot.
        ylabel (str, default='Y' ): y label of the plot.
        zlabel (str, default='Z' ): z label of the plot.
        color (str, default='rainbow'): Matplotlib `Cmap`.
        xScale (str, default='linear'): x axis scale. Can be `linear`, `log`, `symlog`, `logit`.
        yScale (str, default='linear'): y axis scale. Can be `linear`, `log`, `symlog`, `logit`.
        zScale (str, default='linear'): z axis scale. Can be `linear`, `log`, `symlog`, `logit`.
        odeint_method (str, default="scipy"): Selects integration method, by default uses scipy.odeint. `euler` and `rungekutta3` are also available.
    """
    self.sliders = {}
    self.nullclines = []

    self.dF_args = dF_args.copy()                    # dF function's args
    self.dF = dF                                     # Function containing system's equations


    self.MeshDim  = MeshDim
    self.Density = Density                           # Controls concentration of nearby trajectories
    self.Polar = Polar                               # If dF expression given in polar coord. mark as True
    self.Title = Title                               # Title of the plot
    self.xlabel = xlabel                             # Title on X axis
    self.ylabel = ylabel                             # Title on Y axis
    self.zlabel = zlabel

    self.xScale = xScale                             # x axis scale
    self.yScale = yScale                             # y axis scale
    self.zScale = zScale                             # z axis scale
    self.Range = Range                               # Range of graphical representation

    self.streamplot_callback = Streamlines_Velocity_Color_Gradient

    self._create_arrays()

    # Variables for plotting
    self.fig = kargs.get('fig', None)
    if self.fig:
        self.fig.gca().remove()
    else:
        self.fig = plt.figure()

    self.ax = self.fig.add_subplot(projection='3d')
    self.color = color
    self.grid = True

    self.streamplot_args = {"maxLen": maxLen, "odeint_method": odeint_method}


    self.manager = manager.Manager(self)

add_slider(param_name, *, valinit=None, valstep=0.1, valinterval=10)

Adds a slider which can change the value of a parameter in execution time.

Parameters:

Name Type Description Default
param_name str

It takes the name of the parameter on which the slider will be defined. Must be the same as the one appearing as karg in the dF function.

required
valinit float

Initial value of param_name variable. Default value is 0.5.

None
valstep float

Slider step value. Default value is 0.1.

0.1
valinterval float | list[float]

Slider range. Default value is [-10, 10].

10
Source code in phaseportrait/PhasePortrait3D.py
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
def add_slider(self, param_name, *, valinit=None, valstep=0.1, valinterval=10):
    """
    Adds a slider which can change the value of a parameter in execution time.

    Args:
        param_name (str): It takes the name of the parameter on which the slider will be defined. Must be the same as the one appearing as karg in the `dF` function.
        valinit (float, optional): Initial value of *param_name* variable. Default value is 0.5.
        valstep (float, optional): Slider step value. Default value is 0.1.
        valinterval (float|list[float], optional): Slider range. Default value is [-10, 10].
    """

    self.sliders.update({param_name: sliders.Slider(self, param_name, valinit=valinit, valstep=valstep, valinterval=valinterval)})

    self.fig.subplots_adjust(bottom=0.25)

    self.sliders[param_name].slider.on_changed(self.sliders[param_name])

colorbar(toggle=True)

Adds a colorbar for speed.

Parameters:

Name Type Description Default
toggle bool, default=True

If True colorbar is visible.

True
Source code in phaseportrait/PhasePortrait3D.py
202
203
204
205
206
207
208
209
210
211
212
213
214
def colorbar(self, toggle=True):
    """
    Adds a colorbar for speed.

    Args:
        toggle (bool, default=True): If `True` colorbar is visible.
    """
    if (not hasattr(self, "colorbar_ax")) and toggle:
        self.colorbar_ax = None
    else:
        if hasattr(self, "colorbar_ax"):
            self.colorbar_ax.remove()
            del(self.colorbar_ax)

draw_plot(*, color=None, grid=None)

Draws the streamplot. Is intenally used by method plot.

Parameters:

Name Type Description Default
color str, default='viridis'

Matplotlib Cmap.

None
grid bool, default=True

Show grid lines.

None

Returns:

Type Description
matplotlib.Streamplot
Source code in phaseportrait/PhasePortrait3D.py
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
def draw_plot(self, *, color=None, grid=None):
    """
    Draws the streamplot. Is intenally used by method `plot`.

    Args:
        color (str, default='viridis'): Matplotlib `Cmap`.
        grid (bool, default=True): Show grid lines.

    Returns:
        (matplotlib.Streamplot):
    """
    self.dF_args.update({name: slider.value for name, slider in self.sliders.items() if slider.value!= None})

    self._create_arrays()

    try:
        for nullcline in self.nullclines:
            nullcline.plot()
    except AttributeError:
        pass

    if color is not None:
        self.color = color

    # if self.Polar:
    #     self._PolarTransformation()
    # else:
    #     self._dX, self._dY = self.dF(self._X, self._Y, **self.dF_args)

    # if utils.is_number(self._dX):
    #     self._dX = self._X.copy() * 0 + self._dX
    # if utils.is_number(self._dY):
    #     self._dY = self._Y.copy() * 0 + self._dY



    stream = self.streamplot_callback(self.dF, self._X, self._Y, self._Z,
        dF_args=self.dF_args, polar=self.Polar, **self.streamplot_args)

    try:
        norm = stream._velocity_normalization()
    except AttributeError:
        norm = None
    cmap = plt.get_cmap(self.color)

    stream.plot(self.ax, cmap, norm, arrowsize=self.streamplot_args.get('arrow_width', 1))


    self.ax.set_xlim3d(self.Range[0])
    self.ax.set_ylim3d(self.Range[1])
    self.ax.set_zlim3d(self.Range[2])
    # self.ax.set_aspect(abs(self.Range[0,1]-self.Range[0,0])/abs(self.Range[1,1]-self.Range[1,0]))

    self.ax.set_title(f'{self.Title}')
    self.ax.set_xlabel(f'{self.xlabel}')
    self.ax.set_ylabel(f'{self.ylabel}')
    self.ax.set_zlabel(f'{self.zlabel}')
    self.ax.set_xscale(self.xScale)
    self.ax.set_yscale(self.yScale)
    self.ax.set_zscale(self.zScale)




    def log_tick_formatter(val, pos=None):
        return r"$10^{{{:.0f}}}$".format(val)

    if self.xScale in ['log', 'symlog']:
        self.ax.xaxis.set_major_formatter(mticker.FuncFormatter(log_tick_formatter))
    if self.yScale in ['log', 'symlog']:
        self.ax.yaxis.set_major_formatter(mticker.FuncFormatter(log_tick_formatter))
    if self.zScale in ['log', 'symlog']:
        self.ax.zaxis.set_major_formatter(mticker.FuncFormatter(log_tick_formatter))

    self.ax.grid(grid if grid is not None else self.grid)

    return stream

plot(*, color=None, grid=None)

Prepares the plots and computes the values.

Parameters:

Name Type Description Default
color str

Matplotlib colormap.

None
grid bool

Show grid lines.

None

Returns:

Type Description
tuple(matplotlib Figure, matplotlib Axis)

Tuple containing the figure and axis objects.

Source code in phaseportrait/PhasePortrait3D.py
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
def plot(self, *, color=None, grid=None):
    """
    Prepares the plots and computes the values.

    Args:
        color (str): Matplotlib colormap.
        grid (bool): Show grid lines.

    Returns:
        (tuple(matplotlib Figure, matplotlib Axis)): Tuple containing the figure and axis objects.
    """
    # Update plot color and grid settings if specified
    if color is not None:
        self.color = color
    if grid is not None:
        self.grid = grid

    # Draw the plot using the specified color and grid settings
    self.stream = self.draw_plot(color=self.color, grid=grid)

    # Add colorbar if the plot stream has velocity normalization
    if hasattr(self, "colorbar_ax"):
        # Create a colorbar using ScalarMappable with velocity normalization and specified colormap
        cb = plt.colorbar(mplcm.ScalarMappable(
            norm=self.stream._velocity_normalization(), 
            cmap=self.color),
            ax=self.ax,
            cax=self.colorbar_ax)

        # Update colorbar axis attribute
        self.colorbar_ax = cb.ax

    # Redraw the canvas to reflect any changes
    self.fig.canvas.draw_idle()

    # Return the figure and axis objects
    return self.fig, self.ax