Skip to content

PhasePortrait3D

PhasePortrait3D

PhasePortrait3D

Makes a phase portrait of a 3D system.

Examples

image

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
class PhasePortrait3D:
    """
    PhasePortrait3D
    ----------------
    Makes a phase portrait of a 3D system.

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

    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):
        # If scale is log and min range value is 0 or negative the plots is not correct
        _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:
                        _Range[i,j] = abs(max(Range))/100 if j==0 else abs(max(Range))
        self.Range = _Range


        for i, (_P, scale, Range) in enumerate(zip(["_X", "_Y", "_Z"],[self.xScale, self.yScale, self.zScale], self.Range)):
            if scale == 'linear':
                setattr(self, _P, np.linspace(Range[0], Range[1], self.MeshDim))
            if scale == 'log':
                setattr(self, _P, np.logspace(np.log10(Range[0]), np.log10(Range[1]), self.MeshDim))
            if scale == 'symlog':
                setattr(self, _P, np.linspace(Range[0], Range[1], self.MeshDim))

        self._X, self._Y, self._Z = np.meshgrid(self._X, self._Y, self._Z)

        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 `Cmap`.

        Returns:
            (tuple(matplotlib Figure, matplotlib Axis)):
        """
        if color is not None:
            self.color = color
        if grid is not None:
            self.grid = grid

        self.stream = self.draw_plot(color=self.color, grid=grid)

        if hasattr(self, "colorbar_ax"):
            cb = plt.colorbar(mplcm.ScalarMappable(
                norm=self.stream._velocity_normalization(), 
                cmap=self.color),
            ax=self.ax,
            cax=self.colorbar_ax)

            self.colorbar_ax = cb.ax

        self.fig.canvas.draw_idle()

        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.
        """
        if not hasattr(self, "_dR") or not hasattr(self, "_dTheta") or not hasattr(self, "_dPhi"):
            self._R, self._Theta = np.sqrt(self._X**2 + self._Y**2 + self._Z**2), 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._dY, self._dZ = \
            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._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._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
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
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
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
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
148
149
150
151
152
153
154
155
156
157
158
159
160
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
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
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 Cmap.

None

Returns:

Type Description
tuple(matplotlib Figure, matplotlib Axis)
Source code in phaseportrait/PhasePortrait3D.py
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
def plot(self, *, color=None, grid=None):
    """
    Prepares the plots and computes the values.

    Args: 
        color (str): Matplotlib `Cmap`.

    Returns:
        (tuple(matplotlib Figure, matplotlib Axis)):
    """
    if color is not None:
        self.color = color
    if grid is not None:
        self.grid = grid

    self.stream = self.draw_plot(color=self.color, grid=grid)

    if hasattr(self, "colorbar_ax"):
        cb = plt.colorbar(mplcm.ScalarMappable(
            norm=self.stream._velocity_normalization(), 
            cmap=self.color),
        ax=self.ax,
        cax=self.colorbar_ax)

        self.colorbar_ax = cb.ax

    self.fig.canvas.draw_idle()

    return self.fig, self.ax