passagemath-plot 10.6.31rc3__cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of passagemath-plot might be problematic. Click here for more details.

Files changed (82) hide show
  1. passagemath_plot-10.6.31rc3.dist-info/METADATA +172 -0
  2. passagemath_plot-10.6.31rc3.dist-info/RECORD +82 -0
  3. passagemath_plot-10.6.31rc3.dist-info/WHEEL +6 -0
  4. passagemath_plot-10.6.31rc3.dist-info/top_level.txt +2 -0
  5. passagemath_plot.libs/libgfortran-83c28eba.so.5.0.0 +0 -0
  6. passagemath_plot.libs/libgsl-cda90e79.so.28.0.0 +0 -0
  7. passagemath_plot.libs/libopenblasp-r0-6dcb67f9.3.29.so +0 -0
  8. passagemath_plot.libs/libquadmath-2284e583.so.0.0.0 +0 -0
  9. sage/all__sagemath_plot.py +15 -0
  10. sage/ext_data/threejs/animation.css +195 -0
  11. sage/ext_data/threejs/animation.html +85 -0
  12. sage/ext_data/threejs/animation.js +273 -0
  13. sage/ext_data/threejs/fat_lines.js +48 -0
  14. sage/ext_data/threejs/threejs-version.txt +1 -0
  15. sage/ext_data/threejs/threejs_template.html +597 -0
  16. sage/interfaces/all__sagemath_plot.py +1 -0
  17. sage/interfaces/gnuplot.py +196 -0
  18. sage/interfaces/jmoldata.py +208 -0
  19. sage/interfaces/povray.py +56 -0
  20. sage/plot/all.py +42 -0
  21. sage/plot/animate.py +1796 -0
  22. sage/plot/arc.py +504 -0
  23. sage/plot/arrow.py +671 -0
  24. sage/plot/bar_chart.py +205 -0
  25. sage/plot/bezier_path.py +400 -0
  26. sage/plot/circle.py +435 -0
  27. sage/plot/colors.py +1606 -0
  28. sage/plot/complex_plot.cpython-314-x86_64-linux-gnu.so +0 -0
  29. sage/plot/complex_plot.pyx +1446 -0
  30. sage/plot/contour_plot.py +1792 -0
  31. sage/plot/density_plot.py +318 -0
  32. sage/plot/disk.py +373 -0
  33. sage/plot/ellipse.py +375 -0
  34. sage/plot/graphics.py +3580 -0
  35. sage/plot/histogram.py +354 -0
  36. sage/plot/hyperbolic_arc.py +404 -0
  37. sage/plot/hyperbolic_polygon.py +416 -0
  38. sage/plot/hyperbolic_regular_polygon.py +296 -0
  39. sage/plot/line.py +626 -0
  40. sage/plot/matrix_plot.py +629 -0
  41. sage/plot/misc.py +509 -0
  42. sage/plot/multigraphics.py +1294 -0
  43. sage/plot/plot.py +4183 -0
  44. sage/plot/plot3d/all.py +23 -0
  45. sage/plot/plot3d/base.cpython-314-x86_64-linux-gnu.so +0 -0
  46. sage/plot/plot3d/base.pxd +12 -0
  47. sage/plot/plot3d/base.pyx +3378 -0
  48. sage/plot/plot3d/implicit_plot3d.py +659 -0
  49. sage/plot/plot3d/implicit_surface.cpython-314-x86_64-linux-gnu.so +0 -0
  50. sage/plot/plot3d/implicit_surface.pyx +1453 -0
  51. sage/plot/plot3d/index_face_set.cpython-314-x86_64-linux-gnu.so +0 -0
  52. sage/plot/plot3d/index_face_set.pxd +32 -0
  53. sage/plot/plot3d/index_face_set.pyx +1873 -0
  54. sage/plot/plot3d/introduction.py +131 -0
  55. sage/plot/plot3d/list_plot3d.py +649 -0
  56. sage/plot/plot3d/parametric_plot3d.py +1130 -0
  57. sage/plot/plot3d/parametric_surface.cpython-314-x86_64-linux-gnu.so +0 -0
  58. sage/plot/plot3d/parametric_surface.pxd +12 -0
  59. sage/plot/plot3d/parametric_surface.pyx +893 -0
  60. sage/plot/plot3d/platonic.py +601 -0
  61. sage/plot/plot3d/plot3d.py +1442 -0
  62. sage/plot/plot3d/plot_field3d.py +162 -0
  63. sage/plot/plot3d/point_c.pxi +148 -0
  64. sage/plot/plot3d/revolution_plot3d.py +309 -0
  65. sage/plot/plot3d/shapes.cpython-314-x86_64-linux-gnu.so +0 -0
  66. sage/plot/plot3d/shapes.pxd +22 -0
  67. sage/plot/plot3d/shapes.pyx +1382 -0
  68. sage/plot/plot3d/shapes2.py +1512 -0
  69. sage/plot/plot3d/tachyon.py +1779 -0
  70. sage/plot/plot3d/texture.py +453 -0
  71. sage/plot/plot3d/transform.cpython-314-x86_64-linux-gnu.so +0 -0
  72. sage/plot/plot3d/transform.pxd +21 -0
  73. sage/plot/plot3d/transform.pyx +268 -0
  74. sage/plot/plot3d/tri_plot.py +589 -0
  75. sage/plot/plot_field.py +362 -0
  76. sage/plot/point.py +624 -0
  77. sage/plot/polygon.py +562 -0
  78. sage/plot/primitive.py +249 -0
  79. sage/plot/scatter_plot.py +199 -0
  80. sage/plot/step.py +85 -0
  81. sage/plot/streamline_plot.py +328 -0
  82. sage/plot/text.py +432 -0
@@ -0,0 +1,1446 @@
1
+ # sage_setup: distribution = sagemath-plot
2
+ """
3
+ Complex plots
4
+
5
+ AUTHORS:
6
+
7
+ - Robert Bradshaw (2009): initial version
8
+ - David Lowry-Duda (2022): incorporate matplotlib colormaps
9
+ """
10
+ # ****************************************************************************
11
+ # Copyright (C) 2009 Robert Bradshaw <robertwb@math.washington.edu>,
12
+ # Copyright (C) 2022 David Lowry-Duda <david@lowryduda.com>
13
+ #
14
+ # Distributed under the terms of the GNU General Public License (GPL)
15
+ #
16
+ # This code is distributed in the hope that it will be useful,
17
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19
+ # General Public License for more details.
20
+ #
21
+ # The full text of the GPL is available at:
22
+ #
23
+ # https://www.gnu.org/licenses/
24
+ # ****************************************************************************
25
+
26
+ from cysignals.signals cimport sig_on, sig_off, sig_check
27
+
28
+
29
+ # Note: don't import matplotlib at module level. It takes a surprisingly
30
+ # long time to load!
31
+ cimport numpy as cnumpy
32
+
33
+ from sage.plot.primitive import GraphicPrimitive
34
+ from sage.misc.decorators import options
35
+ from sage.rings.complex_double cimport ComplexDoubleElement
36
+ from sage.arith.srange import srange
37
+
38
+ from libc.math cimport hypot, atan2, atan, log, pow, sqrt
39
+ from sage.arith.constants cimport M_PI as PI
40
+
41
+ from sage.libs.gsl.complex cimport *
42
+
43
+
44
+ DEFAULT_LOGARITHMIC_CONTOUR_BASE = 2
45
+ DEFAULT_LINEAR_CONTOUR_BASE = 10
46
+
47
+
48
+ cdef inline ComplexDoubleElement new_CDF_element(double x, double y):
49
+ z = <ComplexDoubleElement>ComplexDoubleElement.__new__(ComplexDoubleElement)
50
+ GSL_SET_COMPLEX(&z._complex, x, y)
51
+ return z
52
+
53
+
54
+ cdef inline double mag_to_lightness(double r, double rate=0.5) noexcept:
55
+ """
56
+ Return a lightness for the given magnitude.
57
+
58
+ Small magnitudes are darker and large magnitudes are lighter, and the
59
+ lightness smoothly transitions.
60
+
61
+ Tweak the rate to adjust how the magnitude affects the color. For
62
+ instance, changing ``rate`` to `1` will cause anything near zero to be
63
+ much darker and poles to be much lighter, while `0.25` would cause the
64
+ reverse effect.
65
+
66
+ INPUT:
67
+
68
+ - ``r`` -- a nonnegative real number; the magnitude
69
+
70
+ - ``rate`` -- a positive real number; how quickly changes in magnitude
71
+ affect changes in output lightness
72
+
73
+ OUTPUT:
74
+
75
+ A value between `-1` (black) and `+1` (white), inclusive.
76
+
77
+ .. SEEALSO::
78
+
79
+ - :func:`sage.plot.complex_plot.cyclic_logarithmic_mag_to_lightness`
80
+ - :func:`sage.plot.complex_plot.mag_and_arg_to_lightness`
81
+
82
+ EXAMPLES:
83
+
84
+ This tests it implicitly::
85
+
86
+ sage: from sage.plot.complex_plot import complex_to_rgb
87
+ sage: complex_to_rgb([[0, 1, 10]]) # abs tol 1e-4
88
+ array([[[0. , 0. , 0. ],
89
+ [0.77172568, 0. , 0. ],
90
+ [1. , 0.22134776, 0.22134776]]])
91
+
92
+ Changing ``rate`` changes the rate of growth::
93
+
94
+ sage: complex_to_rgb([[0, 1, 10]], dark_rate=1) # abs tol 1e-4
95
+ array([[[0. , 0. , 0. ],
96
+ [0.77172568, 0. , 0. ],
97
+ [1. , 0.49693961, 0.49693961]]])
98
+ """
99
+ if rate == 0.5:
100
+ return atan(log(sqrt(r)+1)) * (4/PI) - 1
101
+ else:
102
+ return atan(log(pow(r, rate)+1)) * (4/PI) - 1
103
+
104
+
105
+ cdef inline double cyclic_logarithmic_mag_to_lightness(double r, double base=2) noexcept:
106
+ r"""
107
+ Return a lightness for the given magnitude.
108
+
109
+ This modifies the lightness around magnitudes of size `base^n` for integer
110
+ `n` to create the appearance of logarithmically spaced contours.
111
+
112
+ INPUT:
113
+
114
+ - ``r`` -- a nonnegative real number; the magnitude
115
+
116
+ - ``base`` -- a real number (default: `2`); contours will appear at integer
117
+ powers of ``base``. This should be greater than `1`.
118
+
119
+ OUTPUT:
120
+
121
+ A value between `-1` (black) and `+1` (white), inclusive.
122
+
123
+ .. SEEALSO::
124
+
125
+ - :func:`sage.plot.complex_plot.mag_to_lightness`
126
+ - :func:`sage.plot.complex_plot.mag_and_arg_to_lightness`
127
+ - :func:`sage.plot.complex_plot.cyclic_linear_mag_to_lightness`
128
+
129
+ EXAMPLES:
130
+
131
+ This tests it implicitly::
132
+
133
+ sage: from sage.plot.complex_plot import complex_to_rgb
134
+ sage: complex_to_rgb([[0, 1, 10]], contoured=True, # abs tol 1e-4
135
+ ....: contour_type='logarithmic')
136
+ array([[[1. , 0. , 0. ],
137
+ [1. , 0.15 , 0.15 ],
138
+ [0.98903595, 0. , 0. ]]])
139
+
140
+ We set contours to be multiples of `5` apart::
141
+
142
+ sage: complex_to_rgb([[0, 1, 10]], contoured=True, # abs tol 1e-4
143
+ ....: contour_type='logarithmic', contour_base=5)
144
+ array([[[1. , 0. , 0. ],
145
+ [1. , 0.15 , 0.15 ],
146
+ [0.93466172, 0. , 0. ]]])
147
+ """
148
+ if r < 1e-10:
149
+ return 0.0
150
+ rem = (log(r) / log(base)) % 1
151
+ if rem < 0: # Choose positive mod representative
152
+ rem += 1
153
+ return .15 - rem/2.
154
+
155
+
156
+ cdef inline double cyclic_linear_mag_to_lightness(double r, double base=10) noexcept:
157
+ r"""
158
+ Return a lightness for the given magnitude.
159
+
160
+ This modifies the lightness around magnitudes of size `n*base` for integer
161
+ `n` to create the appearance of linearly spaced contours.
162
+
163
+ INPUT:
164
+
165
+ - ``r`` -- a nonnegative real number; the magnitude
166
+
167
+ - ``base`` -- a positive real number (default: `10`); contours will appear
168
+ at integer multiples of ``base``
169
+
170
+ OUTPUT:
171
+
172
+ A value between `-1` (black) and `+1` (white), inclusive.
173
+
174
+ .. SEEALSO::
175
+
176
+ - :func:`sage.plot.complex_plot.mag_to_lightness`
177
+ - :func:`sage.plot.complex_plot.mag_and_arg_to_lightness`
178
+ - :func:`sage.plot.complex_plot.cyclic_logarithmic_mag_to_lightness`
179
+
180
+ EXAMPLES:
181
+
182
+ This tests it implicitly::
183
+
184
+ sage: from sage.plot.complex_plot import complex_to_rgb
185
+ sage: complex_to_rgb([[1, 5, 11]], contoured=True, # abs tol 1e-4
186
+ ....: contour_type='linear')
187
+ array([[[1. , 0.1, 0.1],
188
+ [0.9, 0. , 0. ],
189
+ [1. , 0.1, 0.1]]])
190
+
191
+ In the above example, note that both `1` and `11` have the same imaginary
192
+ part and are precisely `10` (the default contour separation) apart. If we
193
+ set contours to be multiples of `3` apart, the values are no longer the
194
+ same, but the values for `5` and `11` should be::
195
+
196
+ sage: complex_to_rgb([[1, 5, 11]], contoured=True, # abs tol 1e-4
197
+ ....: contour_type='linear', contour_base=3)
198
+ array([[[0.98333333, 0. , 0. ],
199
+ [0.81666667, 0. , 0. ],
200
+ [0.81666667, 0. , 0. ]]])
201
+ """
202
+ rem = (r / base) % 1
203
+ if rem < 0: # Choose positive mod representative
204
+ rem += 1
205
+ return .15 - rem/2.
206
+
207
+
208
+ cdef inline double mag_and_arg_to_lightness(double r, double arg,
209
+ double base=2, int nphases=10) noexcept:
210
+ r"""
211
+ Return a lightness for the given magnitude and argument.
212
+
213
+ This modifies the lightness around magnitudes of size ``base^n`` for
214
+ integer `n`, and also around arguments of the form `(2 \pi) / nphases * n` for
215
+ integer `n`. This creates the appearance of tiles, bounded by
216
+ (base ``base``) logarithmically spaced magnitude contours and by
217
+ (``nphases`` many) evenly spaced argument contours.
218
+
219
+ INPUT:
220
+
221
+ - ``r`` -- a nonnegative real number
222
+
223
+ - ``arg`` -- a real number
224
+
225
+ - ``base`` -- a positive real number (default: ``2``); contours will appear
226
+ at integer powers of ``base``. This should be greater than `1`.
227
+
228
+ - ``nphases`` -- positive integer (default: `10`); how many phase
229
+ contours to represent a change of argument by `2 \pi`
230
+
231
+ OUTPUT:
232
+
233
+ A value between `-1` (black) and `+1` (white), inclusive.
234
+
235
+ .. SEEALSO::
236
+
237
+ - :func:`sage.plot.complex_plot.mag_to_lightness`
238
+ - :func:`sage.plot.complex_plot.cyclic_logarithmic_mag_to_lightness`
239
+
240
+ EXAMPLES:
241
+
242
+ This tests it implicitly::
243
+
244
+ sage: from sage.plot.complex_plot import complex_to_rgb
245
+ sage: complex_to_rgb([[0, 1, 10]], tiled=True) # abs tol 1e-4
246
+ array([[[1. , 0. , 0. ],
247
+ [1. , 0.15 , 0.15 ],
248
+ [1. , 0.06951798, 0.06951798]]])
249
+ sage: complex_to_rgb([[0, 1 + 1j, -3 - 5j]], tiled=True) # abs tol 1e-4
250
+ array([[[1. , 0. , 0. ],
251
+ [0.9625 , 0.721875 , 0. ],
252
+ [0. , 0.01371897, 0.85409323]]])
253
+
254
+ Adjusting the tiling parameters should create relatively small
255
+ differences::
256
+
257
+ sage: complex_to_rgb([[0, 1 + 1j, -3 - 5j]], # abs tol 1e-4
258
+ ....: tiled=True, nphases=15)
259
+ array([[[1. , 0. , 0. ],
260
+ [0.80625 , 0.6046875 , 0. ],
261
+ [0. , 0.01243417, 0.77410628]]])
262
+ sage: complex_to_rgb([[0, 1 + 1j, -3 - 5j]], # abs tol 1e-4
263
+ ....: tiled=True, contour_base=5, nphases=15)
264
+ array([[[1. , 0. , 0. ],
265
+ [0.87741543, 0.65806157, 0. ],
266
+ [0. , 0.01423401, 0.88615776]]])
267
+ """
268
+ if r < 1e-10:
269
+ return 0.0
270
+ cdef double r_rem, arg_rem
271
+ r_rem = (log(r) / log(base)) % 1
272
+ arg_rem = (nphases * arg / (2*PI)) % 1
273
+ if r_rem < 0: # Choose positive mod representatives
274
+ r_rem += 1
275
+ if arg_rem < 0:
276
+ arg_rem += 1
277
+ return 0.15 - (r_rem)/4. - (arg_rem)/4.
278
+
279
+
280
+ def complex_to_rgb(z_values, contoured=False, tiled=False,
281
+ contour_type='logarithmic', contour_base=None,
282
+ dark_rate=0.5, nphases=10):
283
+ r"""
284
+ Convert a grid of complex numbers to a grid of rgb values using a default
285
+ choice of colors.
286
+
287
+ INPUT:
288
+
289
+ - ``z_values`` -- a grid of complex numbers, as a list of lists
290
+
291
+ - ``contoured`` -- boolean (default: ``False``); causes magnitude to be
292
+ indicated through contour-like adjustments to lightness
293
+
294
+ - ``tiled`` -- boolean (default: ``False``); causes magnitude and argument to
295
+ be indicated through contour-like adjustments to lightness
296
+
297
+ - ``nphases`` -- positive integer (default: 10); when ``tiled=True``,
298
+ this is the number of divisions the phase is divided into
299
+
300
+ - ``contour_type`` -- either ``'logarithmic'``, or ``'linear'`` (default:
301
+ ``'logarithmic'``); causes added contours to be of given type when
302
+ ``contoured=True``.
303
+
304
+ - ``contour_base`` -- positive integer; when ``contour_type`` is
305
+ ``'logarithmic'``, this sets logarithmic contours at multiples of
306
+ ``contour_base`` apart. When ``contour_type`` is ``'linear'``, this sets
307
+ contours at distances of ``contour_base`` apart. If ``None``, then a
308
+ default is chosen depending on ``contour_type``.
309
+
310
+ - ``dark_rate`` -- a positive number (default: 0.5); affects how quickly
311
+ magnitudes affect how light/dark the image is. When there are contours,
312
+ this affects how visible each contour is. Large values (near `1.0`) have
313
+ very strong, immediate effects, while small values (near `0.0`) have
314
+ gradual effects.
315
+
316
+ OUTPUT:
317
+
318
+ An `N \times M \times 3` floating point Numpy array ``X``, where
319
+ ``X[i,j]`` is an (r,g,b) tuple.
320
+
321
+ .. SEEALSO::
322
+
323
+ :func:`sage.plot.complex_plot.complex_to_cmap_rgb`
324
+
325
+ EXAMPLES:
326
+
327
+ We can call this on grids of complex numbers::
328
+
329
+ sage: from sage.plot.complex_plot import complex_to_rgb
330
+ sage: complex_to_rgb([[0, 1, 1000]]) # abs tol 1e-4
331
+ array([[[0. , 0. , 0. ],
332
+ [0.77172568, 0. , 0. ],
333
+ [1. , 0.64421177, 0.64421177]]])
334
+ sage: complex_to_rgb([[0, 1j, 1000j]]) # abs tol 1e-4
335
+ array([[[0. , 0. , 0. ],
336
+ [0.38586284, 0.77172568, 0. ],
337
+ [0.82210588, 1. , 0.64421177]]])
338
+ sage: complex_to_rgb([[0, 1, 1000]], contoured=True) # abs tol 1e-4
339
+ array([[[1. , 0. , 0. ],
340
+ [1. , 0.15 , 0.15 ],
341
+ [0.66710786, 0. , 0. ]]])
342
+ sage: complex_to_rgb([[0, 1, 1000]], tiled=True) # abs tol 1e-4
343
+ array([[[1. , 0. , 0. ],
344
+ [1. , 0.15 , 0.15 ],
345
+ [0.90855393, 0. , 0. ]]])
346
+
347
+ We can change contour types and the distances between contours::
348
+
349
+ sage: complex_to_rgb([[0, 1 + 1j, 3 + 4j]], # abs tol 1e-4
350
+ ....: contoured=True, contour_type='logarithmic', contour_base=3)
351
+ array([[[1. , 0. , 0. ],
352
+ [0.99226756, 0.74420067, 0. ],
353
+ [0.91751324, 0.81245954, 0. ]]])
354
+ sage: complex_to_rgb([[0, 1 + 1j, 3 + 4j]], # abs tol 1e-4
355
+ ....: contoured=True, contour_type='linear', contour_base=3)
356
+ array([[[1. , 0.15 , 0.15 ],
357
+ [0.91429774, 0.6857233 , 0. ],
358
+ [0.81666667, 0.72315973, 0. ]]])
359
+
360
+ Lowering ``dark_rate`` causes colors to go to black more slowly near `0`::
361
+
362
+ sage: complex_to_rgb([[0, 0.5, 1]], dark_rate=0.4) # abs tol 1e-4
363
+ array([[[0. , 0. , 0. ],
364
+ [0.65393731, 0. , 0. ],
365
+ [0.77172568, 0. , 0. ]]])
366
+ sage: complex_to_rgb([[0, 0.5, 1]], dark_rate=0.2) # abs tol 1e-4
367
+ array([[[0. , 0. , 0. ],
368
+ [0.71235886, 0. , 0. ],
369
+ [0.77172568, 0. , 0. ]]])
370
+ """
371
+ import numpy as np
372
+
373
+ cdef unsigned int i, j, imax, jmax
374
+ cdef double x, y, mag, arg
375
+ cdef double lightness, hue, top, bot
376
+ cdef double r, g, b
377
+ cdef int ihue
378
+ cdef ComplexDoubleElement z
379
+ from sage.rings.complex_double import CDF
380
+
381
+ imax = len(z_values)
382
+ jmax = len(z_values[0])
383
+ cdef cnumpy.ndarray[cnumpy.float_t, ndim=3, mode='c'] rgb = np.empty(dtype=float, shape=(imax, jmax, 3))
384
+
385
+ if contour_base is None:
386
+ if contour_type == "linear":
387
+ contour_base = DEFAULT_LINEAR_CONTOUR_BASE
388
+ else:
389
+ contour_base = DEFAULT_LOGARITHMIC_CONTOUR_BASE
390
+ if contour_type not in ("linear", "logarithmic"):
391
+ raise ValueError("Unrecognized contour type argument {}.".format(contour_type))
392
+ if contour_base <= 0:
393
+ raise ValueError("contour_base must be positive")
394
+
395
+ sig_on()
396
+ for i in range(imax):
397
+ row = z_values[i]
398
+ for j in range(jmax):
399
+ zz = row[j]
400
+ if type(zz) is ComplexDoubleElement:
401
+ z = <ComplexDoubleElement>zz
402
+ else:
403
+ z = CDF(zz)
404
+ x = GSL_REAL(z._complex)
405
+ y = GSL_IMAG(z._complex)
406
+ mag = hypot(x, y)
407
+ arg = atan2(y, x) # math module arctan has range from -pi to pi, so cut along negative x-axis
408
+
409
+ if tiled:
410
+ lightness = mag_and_arg_to_lightness(
411
+ mag, arg, base=contour_base, nphases=nphases
412
+ )
413
+ elif contoured:
414
+ if contour_type == "logarithmic":
415
+ lightness = cyclic_logarithmic_mag_to_lightness(mag, base=contour_base)
416
+ else:
417
+ lightness = cyclic_linear_mag_to_lightness(mag, base=contour_base)
418
+ else:
419
+ lightness = mag_to_lightness(mag, rate=dark_rate)
420
+
421
+ if lightness < 0: # in hsv, variable value, full saturation (s=1, v=1+lightness)
422
+ bot = 0
423
+ top = (1+lightness)
424
+ else: # in hsv, variable saturation, full value (v=1, s=1-lightness)
425
+ bot = lightness
426
+ top = 1
427
+
428
+ # Note that does same thing as colorsys module hsv_to_rgb
429
+ # for this setup, but in Cython
430
+ hue = 3*arg/PI
431
+
432
+ if hue < 0:
433
+ hue += 6 # usual hsv hue is thus h=arg/(2*pi) for positive, h=arg/(2*PI)+1 for negative
434
+ ihue = <int>hue
435
+ if ihue == 0:
436
+ r = top
437
+ g = bot + hue * (top-bot)
438
+ b = bot
439
+ elif ihue == 1:
440
+ r = bot + (2-hue) * (top-bot)
441
+ g = top
442
+ b = bot
443
+ elif ihue == 2:
444
+ r = bot
445
+ g = top
446
+ b = bot + (hue-2) * (top-bot)
447
+ elif ihue == 3:
448
+ r = bot
449
+ g = bot + (4-hue) * (top-bot)
450
+ b = top
451
+ elif ihue == 4:
452
+ r = bot + (hue-4) * (top-bot)
453
+ g = bot
454
+ b = top
455
+ else:
456
+ r = top
457
+ g = bot
458
+ b = bot + (6-hue) * (top-bot)
459
+
460
+ rgb[i, j, 0] = r
461
+ rgb[i, j, 1] = g
462
+ rgb[i, j, 2] = b
463
+
464
+ sig_off()
465
+ nan_indices = np.isnan(rgb).any(-1) # Mask for undefined points
466
+ rgb[nan_indices] = 1 # Make nan_indices white
467
+ return rgb
468
+
469
+
470
+ def complex_to_cmap_rgb(z_values, cmap='turbo', contoured=False, tiled=False,
471
+ contour_type='logarithmic', contour_base=None,
472
+ dark_rate=0.5, nphases=10):
473
+ r"""
474
+ Convert a grid of complex numbers to a grid of rgb values using colors
475
+ taken from given colormap.
476
+
477
+ INPUT:
478
+
479
+ - ``z_values`` -- a grid of complex numbers, as a list of lists
480
+
481
+ - ``cmap`` -- the string name of a matplotlib colormap, or an instance
482
+ of a matplotlib Colormap (default: ``'turbo'``)
483
+
484
+ - ``contoured`` -- boolean (default: ``False``); causes magnitude to be
485
+ indicated through contour-like adjustments to lightness
486
+
487
+ - ``tiled`` -- boolean (default: ``False``); causes magnitude and argument to
488
+ be indicated through contour-like adjustments to lightness
489
+
490
+ - ``nphases`` -- positive integer (default: 10); when ``tiled=True``,
491
+ this is the number of divisions the phase is divided into
492
+
493
+ - ``contour_type`` -- either ``'logarithmic'``, or ``'linear'`` (default:
494
+ ``'logarithmic'``); causes added contours to be of given type when
495
+ ``contoured=True``.
496
+
497
+ - ``contour_base`` -- positive integer; when ``contour_type`` is
498
+ ``'logarithmic'``, this sets logarithmic contours at multiples of
499
+ ``contour_base`` apart. When ``contour_type`` is ``'linear'``, this sets
500
+ contours at distances of ``contour_base`` apart. If ``None``, then a
501
+ default is chosen depending on ``contour_type``.
502
+
503
+ - ``dark_rate`` -- a positive number (default: 0.5); affects how quickly
504
+ magnitudes affect how light/dark the image is. When there are contours,
505
+ this affects how visible each contour is. Large values (near 1.0) have
506
+ very strong, immediate effects, while small values (near 0.0) have
507
+ gradual effects.
508
+
509
+ OUTPUT:
510
+
511
+ An `N \times M \times 3` floating point Numpy array ``X``, where
512
+ ``X[i,j]`` is an (r, g, b) tuple.
513
+
514
+ .. SEEALSO::
515
+
516
+ :func:`sage.plot.complex_plot.complex_to_rgb`
517
+
518
+ EXAMPLES:
519
+
520
+ We can call this on grids of complex numbers::
521
+
522
+ sage: from sage.plot.complex_plot import complex_to_cmap_rgb
523
+ sage: complex_to_cmap_rgb([[0, 1, 1000]]) # abs tol 1e-4
524
+ array([[[0. , 0. , 0. ],
525
+ [0.49669808, 0.76400071, 0.18024425],
526
+ [0.87320419, 0.99643856, 0.72730967]]])
527
+ sage: complex_to_cmap_rgb([[0, 1, 1000]], cmap='viridis') # abs tol 1e-4
528
+ array([[[0. , 0. , 0. ],
529
+ [0.0984475 , 0.4375291 , 0.42487821],
530
+ [0.68959896, 0.84592555, 0.84009311]]])
531
+
532
+
533
+ We can change contour types and the distances between contours::
534
+
535
+ sage: complex_to_cmap_rgb([[0, 1 + 1j, 3 + 4j]], contoured=True, # abs tol 1e-4
536
+ ....: contour_type='logarithmic', contour_base=3)
537
+ array([[[0.64362 , 0.98999 , 0.23356 ],
538
+ [0.93239357, 0.81063338, 0.21955399],
539
+ [0.95647342, 0.74861225, 0.14963982]]])
540
+ sage: complex_to_cmap_rgb([[0, 1 + 1j, 3 + 4j]], cmap='turbo', # abs tol 1e-4
541
+ ....: contoured=True, contour_type='linear', contour_base=3)
542
+ array([[[0.71246796, 0.9919238 , 0.3816262 ],
543
+ [0.92617785, 0.79322304, 0.14779989],
544
+ [0.95156284, 0.72025117, 0.05370383]]])
545
+
546
+ We see that changing ``dark_rate`` affects how visible contours are. In this
547
+ example, we set ``contour_base=5`` and note that the points `0` and `1 + i`
548
+ are far away from contours, but `2.9 + 4i` is near (and just below) a
549
+ contour. Raising ``dark_rate`` should have strong effects on the last
550
+ coloration and weaker effects on the others::
551
+
552
+ sage: complex_to_cmap_rgb([[0, 1 + 1j, 2.9 + 4j]], cmap='turbo', # abs tol 1e-4
553
+ ....: contoured=True, dark_rate=0.05, contour_base=5)
554
+ array([[[0.64362 , 0.98999 , 0.23356 ],
555
+ [0.93334746, 0.81330523, 0.23056563],
556
+ [0.96357185, 0.75337736, 0.19440913]]])
557
+ sage: complex_to_cmap_rgb([[0, 1 + 1j, 2.9 + 4j]], cmap='turbo', # abs tol 1e-4
558
+ ....: contoured=True, dark_rate=0.85, contour_base=5)
559
+ array([[[0.64362 , 0.98999 , 0.23356 ],
560
+ [0.93874682, 0.82842892, 0.29289564],
561
+ [0.57778954, 0.42703289, 0.02612716]]])
562
+ """
563
+ import numpy as np
564
+ import matplotlib as mpl
565
+
566
+ if isinstance(cmap, str):
567
+ cmap = mpl.colormaps[cmap]
568
+
569
+ if contour_base is None:
570
+ if contour_type == "linear":
571
+ contour_base = DEFAULT_LINEAR_CONTOUR_BASE
572
+ else:
573
+ contour_base = DEFAULT_LOGARITHMIC_CONTOUR_BASE
574
+ if contour_type not in ("linear", "logarithmic"):
575
+ raise ValueError("Unrecognized contour type argument {}.".format(contour_type))
576
+ if contour_base <= 0:
577
+ raise ValueError("contour_base must be positive")
578
+
579
+ cdef unsigned int i, j, imax, jmax
580
+ cdef double x, y, mag, arg
581
+ cdef double lightness_delta
582
+ cdef ComplexDoubleElement z
583
+ from sage.rings.complex_double import CDF
584
+
585
+ imax = len(z_values)
586
+ jmax = len(z_values[0])
587
+ cdef cnumpy.ndarray[cnumpy.float_t, ndim=3, mode='c'] als = np.empty(dtype=float, shape=(imax, jmax, 2))
588
+
589
+ sig_on()
590
+ for i in range(imax):
591
+ row = z_values[i]
592
+ for j in range(jmax):
593
+ zz = row[j]
594
+ if type(zz) is ComplexDoubleElement:
595
+ z = <ComplexDoubleElement>zz
596
+ else:
597
+ z = CDF(zz)
598
+ x = GSL_REAL(z._complex)
599
+ y = GSL_IMAG(z._complex)
600
+ mag = hypot(x, y)
601
+ arg = atan2(y, x) # math module arctan has range from -pi to pi, so cut along negative x-axis
602
+ if tiled:
603
+ lightness_delta = mag_and_arg_to_lightness(
604
+ mag, arg, base=contour_base, nphases=nphases
605
+ )
606
+ elif contoured:
607
+ if contour_type == "logarithmic":
608
+ lightness_delta = cyclic_logarithmic_mag_to_lightness(mag, base=contour_base)
609
+ else:
610
+ lightness_delta = cyclic_linear_mag_to_lightness(mag, base=contour_base)
611
+ else:
612
+ lightness_delta = mag_to_lightness(mag, rate=dark_rate)
613
+ als[i, j, 0] = arg
614
+ als[i, j, 1] = lightness_delta
615
+ sig_off()
616
+
617
+ args = als[:,:,0]
618
+ nan_indices = np.isnan(als).any(-1) # Mask for undefined points
619
+ normalized_colors = cmap((args + PI) / (2 * PI)) # break on negative reals
620
+ normalized_colors = normalized_colors[:,:,:3] # discard alpha channel
621
+ lightdeltas = als[:,:,1]
622
+
623
+ if tiled or contoured:
624
+ rgbs = add_contours_to_rgb(normalized_colors, lightdeltas, dark_rate=dark_rate)
625
+ else:
626
+ rgbs = add_lightness_smoothing_to_rgb(normalized_colors, lightdeltas)
627
+
628
+ # Apply mask, making nan_indices white
629
+ rgbs[nan_indices] = 1
630
+
631
+ return rgbs
632
+
633
+
634
+ def add_lightness_smoothing_to_rgb(rgb, delta):
635
+ r"""
636
+ Return an rgb array from given array of colors and lightness adjustments.
637
+
638
+ This smoothly adds lightness from black (when ``delta`` is `-1`) to white
639
+ (when ``delta`` is `1`).
640
+
641
+ Each input `(r, g, b)` is modified by ``delta`` to be lighter or darker
642
+ depending on the size of ``delta``. When ``delta`` is `-1`, the output is
643
+ black. When ``delta`` is `+1`, the output is white. Colors
644
+ piecewise-linearly vary from black to the initial `(r, g, b)` to white.
645
+
646
+ We assume that the ``delta`` values come from a function like
647
+ :func:`sage.plot.complex_plot.mag_to_lightness`, which maps magnitudes to
648
+ the range `[-1, +1]`.
649
+
650
+ INPUT:
651
+
652
+ - ``rgb`` -- a grid of length 3 tuples `(r, g, b)`, as an
653
+ `N \times M \times 3` numpy array
654
+
655
+ - ``delta`` -- a grid of values as an `N \times M` numpy array; these
656
+ represent how much to change the lightness of each `(r, g, b)`. Values
657
+ should be in `[-1, 1]`.
658
+
659
+ OUTPUT:
660
+
661
+ An `N \times M \times 3` floating point Numpy array ``X``, where
662
+ ``X[i,j]`` is an (r, g, b) tuple.
663
+
664
+ .. SEEALSO::
665
+
666
+ - :func:`sage.plot.complex_plot.complex_to_rgb`
667
+ - :func:`sage.plot.complex_plot.add_contours_to_rgb`
668
+
669
+ EXAMPLES:
670
+
671
+ We can call this on grids of values::
672
+
673
+ sage: # needs numpy
674
+ sage: import numpy as np
675
+ sage: from sage.plot.complex_plot import add_lightness_smoothing_to_rgb
676
+ sage: add_lightness_smoothing_to_rgb( # abs tol 1e-4
677
+ ....: np.array([[[0, 0.25, 0.5]]]), np.array([[0.75]]))
678
+ array([[[0.75 , 0.8125, 0.875 ]]])
679
+ sage: add_lightness_smoothing_to_rgb( # abs tol 1e-4
680
+ ....: np.array([[[0, 0.25, 0.5]]]), np.array([[0.75]]))
681
+ array([[[0.75 , 0.8125, 0.875 ]]])
682
+ """
683
+ import numpy as np
684
+ delta = delta[:,:,np.newaxis]
685
+ delta_pos = delta > 0.0
686
+ rgb = (1.0 - np.abs(delta))*(rgb - delta_pos) + delta_pos
687
+ rgb = np.clip(rgb, 0.0, 1.0)
688
+ return rgb
689
+
690
+
691
+ def add_contours_to_rgb(rgb, delta, dark_rate=0.5):
692
+ r"""
693
+ Return an rgb array from given array of `(r, g, b)` and `(delta)`.
694
+
695
+ Each input `(r, g, b)` is modified by ``delta`` to be lighter or darker
696
+ depending on the size of ``delta``. Negative ``delta`` values darken the
697
+ color, while positive ``delta`` values lighten the pixel.
698
+
699
+ We assume that the ``delta`` values come from a function like
700
+ :func:`sage.plot.complex_plot.mag_to_lightness`, which maps magnitudes to
701
+ the range `[-1, +1]`.
702
+
703
+ INPUT:
704
+
705
+ - ``rgb`` -- a grid of length 3 tuples `(r, g, b)`, as an `N \times M
706
+ \times 3` numpy array
707
+
708
+ - ``delta`` -- a grid of values as an `N \times M` numpy array; these
709
+ represent how much to change the lightness of each `(r, g, b)`. Values
710
+ should be in `[-1, 1]`.
711
+
712
+ - ``dark_rate`` -- a positive number (default: 0.5); affects how
713
+ strongly visible the contours appear
714
+
715
+ OUTPUT:
716
+
717
+ An `N \times M \times 3` floating point Numpy array ``X``, where
718
+ ``X[i,j]`` is an (r, g, b) tuple.
719
+
720
+ .. SEEALSO::
721
+
722
+ - :func:`sage.plot.complex_plot.complex_to_rgb`,
723
+ - :func:`sage.plot.complex_plot.add_lightness_smoothing_to_rgb`
724
+
725
+ ALGORITHM:
726
+
727
+ Each pixel and lightness-delta is mapped from `(r, g, b, delta) \mapsto
728
+ (h, l, s, delta)` using the standard RGB-to-HLS formula.
729
+
730
+ Then the lightness is adjusted via `l \mapsto l' = l + 0.5 \cdot delta`.
731
+
732
+ Finally map `(h, l', s) \mapsto (r, g, b)` using the standard HLS-to-RGB
733
+ formula.
734
+
735
+ EXAMPLES::
736
+
737
+ sage: # needs numpy
738
+ sage: import numpy as np
739
+ sage: from sage.plot.complex_plot import add_contours_to_rgb
740
+ sage: add_contours_to_rgb(np.array([[[0, 0.25, 0.5]]]), # abs tol 1e-4
741
+ ....: np.array([[0.75]]))
742
+ array([[[0.25 , 0.625, 1. ]]])
743
+ sage: add_contours_to_rgb(np.array([[[0, 0, 0]]]), # abs tol 1e-4
744
+ ....: np.array([[1]]))
745
+ array([[[0.5, 0.5, 0.5]]])
746
+ sage: add_contours_to_rgb(np.array([[[1, 1, 1]]]), # abs tol 1e-4
747
+ ....: np.array([[-0.5]]))
748
+ array([[[0.75, 0.75, 0.75]]])
749
+
750
+ Raising ``dark_rate`` leads to bigger adjustments::
751
+
752
+ sage: add_contours_to_rgb(np.array([[[0.5, 0.5, 0.5]]]), # abs tol 1e-4 # needs numpy
753
+ ....: np.array([[0.5]]), dark_rate=0.1)
754
+ array([[[0.55, 0.55, 0.55]]])
755
+ sage: add_contours_to_rgb(np.array([[[0.5, 0.5, 0.5]]]), # abs tol 1e-4 # needs numpy
756
+ ....: np.array([[0.5]]), dark_rate=0.5)
757
+ array([[[0.75, 0.75, 0.75]]])
758
+ """
759
+ import numpy as np
760
+ hls = rgb_to_hls(rgb)
761
+ hls[..., 1] += dark_rate * delta
762
+ hls = np.clip(hls, 0.0, 1.0)
763
+ rgb = hls_to_rgb(hls)
764
+ return rgb
765
+
766
+
767
+ class ComplexPlot(GraphicPrimitive):
768
+ """
769
+ The GraphicsPrimitive to display complex functions in using the domain
770
+ coloring method
771
+
772
+ INPUT:
773
+
774
+ - ``rgb_data`` -- an array of colored points to be plotted
775
+
776
+ - ``x_range`` -- a minimum and maximum x value for the plot
777
+
778
+ - ``y_range`` -- a minimum and maximum y value for the plot
779
+
780
+ TESTS::
781
+
782
+ sage: p = complex_plot(lambda z: z^2-1, (-2, 2), (-2, 2))
783
+ """
784
+ def __init__(self, rgb_data, x_range, y_range, options):
785
+ """
786
+ TESTS::
787
+
788
+ sage: p = complex_plot(lambda z: z^2-1, (-2, 2), (-2, 2))
789
+ """
790
+ self.x_range = x_range
791
+ self.y_range = y_range
792
+ self.x_count = len(rgb_data)
793
+ self.y_count = len(rgb_data[0])
794
+ self.rgb_data = rgb_data
795
+ GraphicPrimitive.__init__(self, options)
796
+
797
+ def get_minmax_data(self):
798
+ """
799
+ Return a dictionary with the bounding box data.
800
+
801
+ EXAMPLES::
802
+
803
+ sage: p = complex_plot(lambda z: z, (-1, 2), (-3, 4))
804
+ sage: sorted(p.get_minmax_data().items())
805
+ [('xmax', 2.0), ('xmin', -1.0), ('ymax', 4.0), ('ymin', -3.0)]
806
+ sage: p = complex_plot(lambda z: z, (1, 2), (3, 4))
807
+ sage: sorted(p.get_minmax_data().items())
808
+ [('xmax', 2.0), ('xmin', 1.0), ('ymax', 4.0), ('ymin', 3.0)]
809
+ """
810
+ from sage.plot.plot import minmax_data
811
+ return minmax_data(self.x_range, self.y_range, dict=True)
812
+
813
+ def _allowed_options(self):
814
+ """
815
+ TESTS::
816
+
817
+ sage: isinstance(complex_plot(lambda z: z, (-1,1), (-1,1))[0]._allowed_options(), dict)
818
+ True
819
+ """
820
+ return {'plot_points':'How many points to use for plotting precision',
821
+ 'interpolation':'What interpolation method to use'}
822
+
823
+ def _repr_(self):
824
+ """
825
+ TESTS::
826
+
827
+ sage: isinstance(complex_plot(lambda z: z, (-1,1), (-1,1))[0]._repr_(), str)
828
+ True
829
+ """
830
+ return "ComplexPlot defined by a %s x %s data grid" % (self.x_count, self.y_count)
831
+
832
+ def _render_on_subplot(self, subplot):
833
+ """
834
+ TESTS::
835
+
836
+ sage: complex_plot(lambda x: x^2, (-5, 5), (-5, 5))
837
+ Graphics object consisting of 1 graphics primitive
838
+ """
839
+ options = self.options()
840
+ x0, x1 = float(self.x_range[0]), float(self.x_range[1])
841
+ y0, y1 = float(self.y_range[0]), float(self.y_range[1])
842
+ subplot.imshow(self.rgb_data, origin='lower', extent=(x0, x1, y0, y1),
843
+ interpolation=options['interpolation'])
844
+
845
+
846
+ @options(plot_points=100, interpolation='catrom')
847
+ def complex_plot(f, x_range, y_range, contoured=False, tiled=False, cmap=None,
848
+ contour_type='logarithmic', contour_base=None, dark_rate=0.5,
849
+ nphases=10, **options):
850
+ r"""
851
+ ``complex_plot`` takes a complex function of one variable,
852
+ `f(z)` and plots output of the function over the specified
853
+ ``x_range`` and ``y_range`` as demonstrated below. The magnitude of
854
+ the output is indicated by the brightness and the argument is
855
+ represented by the hue.
856
+
857
+ By default, zero magnitude corresponds to black output, infinite
858
+ magnitude corresponds to white output. The options ``contoured``,
859
+ ``tiled``, and ``cmap`` affect the output.
860
+
861
+ ``complex_plot(f, (xmin, xmax), (ymin, ymax), contoured, tiled, cmap, ...)``
862
+
863
+ INPUT:
864
+
865
+ - ``f`` -- a function of a single complex value `x + iy`
866
+
867
+ - ``(xmin, xmax)`` -- 2-tuple, the range of ``x`` values
868
+
869
+ - ``(ymin, ymax)`` -- 2-tuple, the range of ``y`` values
870
+
871
+ - ``cmap`` -- ``None``, or the string name of a matplotlib colormap, or an
872
+ instance of a matplotlib Colormap, or the special string ``'matplotlib'``
873
+ (default: ``None``); if ``None``, then hues are chosen from a standard
874
+ color wheel, cycling from red to yellow to blue. If ``matplotlib``, then
875
+ hues are chosen from a preset matplotlib colormap.
876
+
877
+ The following named parameter inputs can be used to add contours and adjust
878
+ their distribution:
879
+
880
+ - ``contoured`` -- boolean (default: ``False``); causes the magnitude
881
+ to be indicated by logarithmically spaced 'contours'. The
882
+ magnitude along one contour is either twice or half the magnitude
883
+ along adjacent contours.
884
+
885
+ - ``dark_rate`` -- a positive number (default: 0.5); affects how quickly
886
+ magnitudes affect how light/dark the image is. When there are contours,
887
+ this affects how visible each contour is. Large values (near 1.0) have
888
+ very strong, immediate effects, while small values (near 0.0) have
889
+ gradual effects.
890
+
891
+ - ``tiled`` -- boolean (default: ``False``); causes the magnitude to
892
+ be indicated by logarithmically spaced 'contours' as in
893
+ ``contoured``, and in addition for there to be `10` evenly
894
+ spaced phase contours.
895
+
896
+ - ``nphases`` -- positive integer (default: 10); when ``tiled=True``,
897
+ this is the number of divisions the phase is divided into
898
+
899
+ - ``contour_type`` -- either ``'logarithmic'``, or ``'linear'`` (default:
900
+ ``'logarithmic'``); causes added contours to be of given type when
901
+ ``contoured=True``.
902
+
903
+ - ``contour_base`` -- positive integer; when ``contour_type`` is
904
+ ``'logarithmic'``, this sets logarithmic contours at multiples of
905
+ ``contour_base`` apart. When ``contour_type`` is ``'linear'``, this sets
906
+ contours at distances of ``contour_base`` apart. If ``None``, then a
907
+ default is chosen depending on ``contour_type``.
908
+
909
+ The following inputs may also be passed in as named parameters:
910
+
911
+ - ``plot_points`` -- integer (default: 100); number of points to
912
+ plot in each direction of the grid
913
+
914
+ - ``interpolation`` -- string (default: ``'catrom'``); the interpolation
915
+ method to use: ``'bilinear'``, ``'bicubic'``, ``'spline16'``,
916
+ ``'spline36'``, ``'quadric'``, ``'gaussian'``, ``'sinc'``,
917
+ ``'bessel'``, ``'mitchell'``, ``'lanczos'``, ``'catrom'``,
918
+ ``'hermite'``, ``'hanning'``, ``'hamming'``, ``'kaiser'``
919
+
920
+ Any additional parameters will be passed to ``show()``, as long as they're
921
+ valid.
922
+
923
+ .. NOTE::
924
+
925
+ Matplotlib colormaps can be chosen or customized to cater to different
926
+ types of vision. The colormaps 'cividis' and 'viridis' in matplotlib
927
+ are designed to be perceptually uniform to a broader audience. The
928
+ colormap 'turbo' is similar to the default but with more even contrast.
929
+ See [NAR2018]_ for more information about colormap choice for
930
+ scientific visualization.
931
+
932
+ EXAMPLES:
933
+
934
+ Here we plot a couple of simple functions::
935
+
936
+ sage: complex_plot(sqrt(x), (-5, 5), (-5, 5)) # needs sage.symbolic
937
+ Graphics object consisting of 1 graphics primitive
938
+
939
+ .. PLOT::
940
+
941
+ sphinx_plot(complex_plot(sqrt(x), (-5, 5), (-5, 5)))
942
+
943
+ ::
944
+
945
+ sage: complex_plot(sin(x), (-5, 5), (-5, 5)) # needs sage.symbolic
946
+ Graphics object consisting of 1 graphics primitive
947
+
948
+ .. PLOT::
949
+
950
+ sphinx_plot(complex_plot(sin(x), (-5, 5), (-5, 5)))
951
+
952
+ ::
953
+
954
+ sage: complex_plot(log(x), (-10, 10), (-10, 10)) # needs sage.symbolic
955
+ Graphics object consisting of 1 graphics primitive
956
+
957
+ .. PLOT::
958
+
959
+ sphinx_plot(complex_plot(log(x), (-10, 10), (-10, 10)))
960
+
961
+ ::
962
+
963
+ sage: complex_plot(exp(x), (-10, 10), (-10, 10)) # needs sage.symbolic
964
+ Graphics object consisting of 1 graphics primitive
965
+
966
+ .. PLOT::
967
+
968
+ sphinx_plot(complex_plot(exp(x), (-10, 10), (-10, 10)))
969
+
970
+ A plot with a different choice of colormap::
971
+
972
+ sage: complex_plot(exp(x), (-10, 10), (-10, 10), cmap='viridis') # needs sage.symbolic
973
+ Graphics object consisting of 1 graphics primitive
974
+
975
+ .. PLOT::
976
+
977
+ sphinx_plot(complex_plot(exp(x), (-10, 10), (-10, 10), cmap='viridis'))
978
+
979
+ A function with some nice zeros and a pole::
980
+
981
+ sage: f(z) = z^5 + z - 1 + 1/z # needs sage.symbolic
982
+ sage: complex_plot(f, (-3, 3), (-3, 3)) # needs sage.symbolic
983
+ Graphics object consisting of 1 graphics primitive
984
+
985
+ .. PLOT::
986
+
987
+ def f(z): return z**5 + z - 1 + 1/z
988
+ sphinx_plot(complex_plot(f, (-3, 3), (-3, 3)))
989
+
990
+ The same function as above, but with contours. Contours render poorly with
991
+ few plot points, so we use 300 here::
992
+
993
+ sage: f(z) = z^5 + z - 1 + 1/z # needs sage.symbolic
994
+ sage: complex_plot(f, (-3, 3), (-3, 3), plot_points=300, contoured=True) # needs sage.symbolic
995
+ Graphics object consisting of 1 graphics primitive
996
+
997
+ .. PLOT::
998
+
999
+ def f(z): return z**5 + z - 1 + 1/z
1000
+ sphinx_plot(complex_plot(f, (-3, 3), (-3, 3), plot_points=300, contoured=True))
1001
+
1002
+ The same function as above, but tiled and with the *plasma* colormap::
1003
+
1004
+ sage: f(z) = z^5 + z - 1 + 1/z # needs sage.symbolic
1005
+ sage: complex_plot(f, (-3, 3), (-3, 3), # needs sage.symbolic
1006
+ ....: plot_points=300, tiled=True, cmap='plasma')
1007
+ Graphics object consisting of 1 graphics primitive
1008
+
1009
+ .. PLOT::
1010
+
1011
+ def f(z): return z**5 + z - 1 + 1/z
1012
+ sphinx_plot(complex_plot(f, (-3, 3), (-3, 3), plot_points=300, tiled=True, cmap='plasma'))
1013
+
1014
+ When using ``tiled=True``, the number of phase subdivisions can be
1015
+ controlled by adjusting ``nphases``. We make the same plot with fewer
1016
+ tilings::
1017
+
1018
+ sage: f(z) = z^5 + z - 1 + 1/z # needs sage.symbolic
1019
+ sage: complex_plot(f, (-3, 3), (-3, 3), plot_points=300, # needs sage.symbolic
1020
+ ....: tiled=True, nphases=5, cmap='plasma')
1021
+ Graphics object consisting of 1 graphics primitive
1022
+
1023
+ .. PLOT::
1024
+
1025
+ def f(z): return z**5 + z - 1 + 1/z
1026
+ sphinx_plot(complex_plot(f, (-3, 3), (-3, 3), plot_points=300, tiled=True, nphases=5, cmap='plasma'))
1027
+
1028
+ It is also possible to use *linear* contours. We plot the same function
1029
+ above on an inset, setting contours to appear `1` apart::
1030
+
1031
+ sage: f(z) = z^5 + z - 1 + 1/z # needs sage.symbolic
1032
+ sage: complex_plot(f, (0, 1), (0, 1), plot_points=300, # needs sage.symbolic
1033
+ ....: contoured=True, contour_type='linear', contour_base=1)
1034
+ Graphics object consisting of 1 graphics primitive
1035
+
1036
+ .. PLOT::
1037
+
1038
+ def f(z): return z**5 + z - 1 + 1/z
1039
+ sphinx_plot(complex_plot(f, (0, 1), (0, 1), plot_points=300, contoured=True, contour_type='linear', contour_base=1))
1040
+
1041
+ Note that tightly spaced contours can lead to Moiré patterns and aliasing
1042
+ problems. For example::
1043
+
1044
+ sage: f(z) = z^5 + z - 1 + 1/z # needs sage.symbolic
1045
+ sage: complex_plot(f, (-3, 3), (-3, 3), plot_points=300, # needs sage.symbolic
1046
+ ....: contoured=True, contour_type='linear', contour_base=1)
1047
+ Graphics object consisting of 1 graphics primitive
1048
+
1049
+ .. PLOT::
1050
+
1051
+ def f(z): return z**5 + z - 1 + 1/z
1052
+ sphinx_plot(complex_plot(f, (-3, 3), (-3, 3), plot_points=300, contoured=True, contour_type='linear', contour_base=1))
1053
+
1054
+ When choosing colormaps, cyclic colormaps such as *twilight* or *hsv* might
1055
+ be considered more appropriate for showing changes in phase without sharp
1056
+ color contrasts::
1057
+
1058
+ sage: f(z) = z^5 + z - 1 + 1/z # needs sage.symbolic
1059
+ sage: complex_plot(f, (-3, 3), (-3, 3), plot_points=300, cmap='twilight') # needs sage.symbolic
1060
+ Graphics object consisting of 1 graphics primitive
1061
+
1062
+ .. PLOT::
1063
+
1064
+ def f(z): return z**5 + z - 1 + 1/z
1065
+ sphinx_plot(complex_plot(f, (-3, 3), (-3, 3), plot_points=300, cmap='twilight'))
1066
+
1067
+ Passing *matplotlib* as the colormap gives a special colormap that is
1068
+ similar to the default::
1069
+
1070
+ sage: f(z) = z^5 + z - 1 + 1/z # needs sage.symbolic
1071
+ sage: complex_plot(f, (-3, 3), (-3, 3), # needs sage.symbolic
1072
+ ....: plot_points=300, contoured=True, cmap='matplotlib')
1073
+ Graphics object consisting of 1 graphics primitive
1074
+
1075
+ .. PLOT::
1076
+
1077
+ def f(z): return z**5 + z - 1 + 1/z
1078
+ sphinx_plot(complex_plot(f, (-3, 3), (-3, 3), plot_points=300, contoured=True, cmap='matplotlib'))
1079
+
1080
+ Here is the identity, useful for seeing what values map to what colors::
1081
+
1082
+ sage: complex_plot(lambda z: z, (-3, 3), (-3, 3)) # needs sage.symbolic
1083
+ Graphics object consisting of 1 graphics primitive
1084
+
1085
+ .. PLOT::
1086
+
1087
+ sphinx_plot(complex_plot(lambda z: z, (-3, 3), (-3, 3)))
1088
+
1089
+ The Riemann Zeta function::
1090
+
1091
+ sage: complex_plot(zeta, (-30,30), (-30,30)) # needs sage.symbolic
1092
+ Graphics object consisting of 1 graphics primitive
1093
+
1094
+ .. PLOT::
1095
+
1096
+ sphinx_plot(complex_plot(zeta, (-30,30), (-30,30)))
1097
+
1098
+ For advanced usage, it is possible to tweak many parameters. Increasing
1099
+ ``dark_rate`` will make regions become darker/lighter faster when there are no
1100
+ contours::
1101
+
1102
+ sage: complex_plot(zeta, (-30, 30), (-30, 30), dark_rate=1.0) # needs sage.symbolic
1103
+ Graphics object consisting of 1 graphics primitive
1104
+
1105
+ .. PLOT::
1106
+
1107
+ sphinx_plot(complex_plot(zeta, (-30,30), (-30,30), dark_rate=1.0))
1108
+
1109
+ Decreasing ``dark_rate`` has the opposite effect. When there are contours,
1110
+ adjust ``dark_rate`` affects how visible contours are. Compare::
1111
+
1112
+ sage: complex_plot(zeta, (-1, 9), (10, 20), plot_points=200, # long time, needs sage.symbolic
1113
+ ....: contoured=True, cmap='twilight', dark_rate=0.2)
1114
+ Graphics object consisting of 1 graphics primitive
1115
+
1116
+ .. PLOT::
1117
+
1118
+ sphinx_plot(complex_plot(zeta, (-1, 9), (10, 20), plot_points=200, contoured=True, cmap='twilight', dark_rate=0.2))
1119
+
1120
+ and::
1121
+
1122
+ sage: complex_plot(zeta, (-1, 9), (10, 20), plot_points=200, # long time, needs sage.symbolic
1123
+ ....: contoured=True, cmap='twilight', dark_rate=0.75)
1124
+ Graphics object consisting of 1 graphics primitive
1125
+
1126
+ .. PLOT::
1127
+
1128
+ sphinx_plot(complex_plot(zeta, (-1, 9), (10, 20), plot_points=200, contoured=True, cmap='twilight', dark_rate=0.75))
1129
+
1130
+ In practice, different values of ``dark_rate`` will work well with
1131
+ different colormaps.
1132
+
1133
+ Extra options will get passed on to show(), as long as they are valid::
1134
+
1135
+ sage: complex_plot(lambda z: z, (-3, 3), (-3, 3), figsize=[1,1]) # needs sage.symbolic
1136
+ Graphics object consisting of 1 graphics primitive
1137
+
1138
+ ::
1139
+
1140
+ sage: complex_plot(lambda z: z, (-3, 3), (-3, 3)).show(figsize=[1,1]) # These are equivalent # needs sage.symbolic
1141
+
1142
+ REFERENCES:
1143
+
1144
+ Plotting complex functions with colormaps follows the strategy
1145
+ from [LD2021]_ and incorporates contour techniques described
1146
+ in [WegSem2010]_.
1147
+
1148
+ TESTS:
1149
+
1150
+ Test to make sure that using fast_callable functions works::
1151
+
1152
+ sage: # needs sage.symbolic
1153
+ sage: f(x) = x^2
1154
+ sage: g = fast_callable(f, domain=CC, vars='x')
1155
+ sage: h = fast_callable(f, domain=CDF, vars='x')
1156
+ sage: P = complex_plot(f, (-10, 10), (-10, 10))
1157
+ sage: Q = complex_plot(g, (-10, 10), (-10, 10))
1158
+ sage: R = complex_plot(h, (-10, 10), (-10, 10))
1159
+ sage: S = complex_plot(exp(x)-sin(x), (-10, 10), (-10, 10))
1160
+ sage: P; Q; R; S
1161
+ Graphics object consisting of 1 graphics primitive
1162
+ Graphics object consisting of 1 graphics primitive
1163
+ Graphics object consisting of 1 graphics primitive
1164
+ Graphics object consisting of 1 graphics primitive
1165
+
1166
+ Test to make sure symbolic functions still work without declaring
1167
+ a variable. (We don't do this in practice because it doesn't use
1168
+ fast_callable, so it is much slower.)
1169
+
1170
+ ::
1171
+
1172
+ sage: complex_plot(sqrt, (-5, 5), (-5, 5)) # needs sage.symbolic
1173
+ Graphics object consisting of 1 graphics primitive
1174
+ """
1175
+ import matplotlib as mpl
1176
+ import numpy as np
1177
+ from sage.plot.all import Graphics
1178
+ from sage.plot.misc import setup_for_eval_on_grid
1179
+ from sage.ext.fast_callable import fast_callable
1180
+ from sage.rings.complex_double import CDF
1181
+
1182
+ try:
1183
+ f = fast_callable(f, domain=CDF, expect_one_var=True)
1184
+ except (AttributeError, TypeError, ValueError):
1185
+ pass
1186
+
1187
+ cdef double x, y
1188
+ _, ranges = setup_for_eval_on_grid([], [x_range, y_range],
1189
+ options['plot_points'])
1190
+ x_range = ranges[0]
1191
+ y_range = ranges[1]
1192
+ cdef list z_values = []
1193
+ cdef list row
1194
+ for y in srange(*y_range, include_endpoint=True):
1195
+ row = []
1196
+ for x in srange(*x_range, include_endpoint=True):
1197
+ sig_check()
1198
+ row.append(f(new_CDF_element(x, y)))
1199
+ z_values.append(row)
1200
+
1201
+ if cmap is None:
1202
+ # produce colors using the established default method
1203
+ rgbs = complex_to_rgb(
1204
+ z_values, contoured=contoured, tiled=tiled,
1205
+ contour_type=contour_type, contour_base=contour_base,
1206
+ dark_rate=dark_rate, nphases=nphases
1207
+ )
1208
+ else:
1209
+ # choose colors from colormap
1210
+ if isinstance(cmap, str):
1211
+ if cmap == 'matplotlib':
1212
+ domain = np.linspace(0, 1, 256)
1213
+ shifted_domain = np.roll(domain, 128)
1214
+ default_cmap = mpl.colors.LinearSegmentedColormap.from_list(
1215
+ "sage_default", mpl.colormaps['hsv'](shifted_domain)
1216
+ )
1217
+ cmap = default_cmap
1218
+ else:
1219
+ cmap = mpl.colormaps[cmap]
1220
+ rgbs = complex_to_cmap_rgb(
1221
+ z_values, cmap=cmap, contoured=contoured, tiled=tiled,
1222
+ contour_type=contour_type, contour_base=contour_base,
1223
+ dark_rate=dark_rate, nphases=nphases
1224
+ )
1225
+
1226
+ g = Graphics()
1227
+ g._set_extra_kwds(Graphics._extract_kwds_for_show(options, ignore=['xmin', 'xmax']))
1228
+ g.add_primitive(ComplexPlot(rgbs, x_range[:2], y_range[:2], options))
1229
+ return g
1230
+
1231
+
1232
+ def rgb_to_hls(rgb):
1233
+ r"""
1234
+ Convert array of rgb values (each in the range `[0, 1]`)
1235
+ to a numpy array of hls values (each in the range `[0, 1]`)
1236
+
1237
+ INPUT:
1238
+
1239
+ - ``rgb`` -- an `N \times 3` array of floats with values
1240
+ in the range `[0, 1]`; the rgb values at each point. (Note that the input
1241
+ can actually be of any dimension, such as `N \times M \times 3`, as long
1242
+ as the last dimension has length `3`).
1243
+
1244
+ OUTPUT:
1245
+
1246
+ An `N \times 3` Numpy array of floats in the range `[0, 1]`, with
1247
+ the same dimensions as the input array.
1248
+
1249
+ .. SEEALSO::
1250
+
1251
+ :func:`sage.plot.complex_plot.hls_to_rgb`
1252
+
1253
+ EXAMPLES:
1254
+
1255
+ We convert a row of floats and verify that we can convert back using
1256
+ ``hls_to_rgb``::
1257
+
1258
+ sage: from sage.plot.complex_plot import rgb_to_hls, hls_to_rgb
1259
+ sage: rgb = [[0.2, 0.4, 0.5], [0.1, 0.3, 1.0]]
1260
+ sage: hls = rgb_to_hls(rgb)
1261
+ sage: hls # abs tol 1e-4
1262
+ array([[0.55555556, 0.35 , 0.42857143],
1263
+ [0.62962963, 0.55 , 1. ]])
1264
+ sage: hls_to_rgb(hls) # abs tol 1e-4
1265
+ array([[0.2, 0.4, 0.5],
1266
+ [0.1, 0.3, 1. ]])
1267
+
1268
+ Multidimensional inputs can be given as well::
1269
+
1270
+ sage: multidim_arr = [[[0, 0.2, 0.4], [1, 1, 1]], [[0, 0, 0], [0.5, 0.6, 0.9]]]
1271
+ sage: rgb_to_hls(multidim_arr) # abs tol 1e-4
1272
+ array([[[0.58333333, 0.2 , 1. ],
1273
+ [0. , 1. , 0. ]],
1274
+ [[0. , 0. , 0. ],
1275
+ [0.625 , 0.7 , 0.66666667]]])
1276
+ """
1277
+ # Notation and algorithm corresponds to colorsys.rgb_to_hls
1278
+ import numpy as np
1279
+ rgb = np.asarray(rgb)
1280
+ if rgb.shape[-1] != 3:
1281
+ raise ValueError("Last dimension of input array must be 3; "
1282
+ "shape {} was found.".format(rgb.shape))
1283
+ in_shape = rgb.shape
1284
+ rgb = np.asarray(rgb, dtype=np.dtype(float))
1285
+ rgb_max = rgb.max(-1)
1286
+ rgb_min = rgb.min(-1)
1287
+ l = (rgb_max + rgb_min)/2.0 # lightness
1288
+
1289
+ hls = np.zeros_like(rgb)
1290
+ delta = np.ptp(rgb, -1)
1291
+ s = np.zeros_like(delta)
1292
+
1293
+ ipos = delta > 0
1294
+ idx = (l <= 0.5) & ipos
1295
+ s[idx] = delta[idx] / (rgb_max[idx] + rgb_min[idx])
1296
+
1297
+ idx = (l > 0.5) & ipos
1298
+ s[idx] = delta[idx] / (2.0 - rgb_max[idx] - rgb_min[idx]) # saturation
1299
+
1300
+ # red is max
1301
+ idx = (rgb[..., 0] == rgb_max) & ipos
1302
+ hls[idx, 0] = (rgb[idx, 1] - rgb[idx, 2]) / delta[idx]
1303
+
1304
+ # green is max
1305
+ idx = (rgb[..., 1] == rgb_max) & ipos
1306
+ hls[idx, 0] = 2.0 + (rgb[idx, 2] - rgb[idx, 0]) / delta[idx]
1307
+
1308
+ # blue is max
1309
+ idx = (rgb[..., 2] == rgb_max) & ipos
1310
+ hls[idx, 0] = 4.0 + (rgb[idx, 0] - rgb[idx, 1]) / delta[idx]
1311
+
1312
+ hls[..., 0] = (hls[..., 0] / 6.0) % 1.0
1313
+ hls[..., 1] = l
1314
+ hls[..., 2] = s
1315
+
1316
+ return hls.reshape(in_shape)
1317
+
1318
+
1319
+ def hls_to_rgb(hls):
1320
+ r"""
1321
+ Convert array of hls values (each in the range `[0, 1]`)
1322
+ to a numpy array of rgb values (each in the range `[0, 1]`)
1323
+
1324
+ INPUT:
1325
+
1326
+ - ``hls`` -- an `N \times 3` array of floats in the range `[0, 1]`; the hls
1327
+ values at each point. (Note that the input can actually be of any
1328
+ dimension, such as `N \times M \times 3`, as long as the last dimension
1329
+ has length `3`).
1330
+
1331
+ OUTPUT:
1332
+
1333
+ An `N \times 3` Numpy array of floats in the range `[0, 1]`, with
1334
+ the same dimensions as the input array.
1335
+
1336
+ .. SEEALSO::
1337
+
1338
+ :func:`sage.plot.complex_plot.rgb_to_hls`
1339
+
1340
+ EXAMPLES:
1341
+
1342
+ We convert a row of floats and verify that we can convert back using
1343
+ ``rgb_to_hls``::
1344
+
1345
+ sage: from sage.plot.complex_plot import rgb_to_hls, hls_to_rgb
1346
+ sage: hls = [[0.2, 0.4, 0.5], [0.1, 0.3, 1.0]]
1347
+ sage: rgb = hls_to_rgb(hls)
1348
+ sage: rgb # abs tol 1e-4
1349
+ array([[0.52, 0.6 , 0.2 ],
1350
+ [0.6 , 0.36, 0. ]])
1351
+ sage: rgb_to_hls(rgb) # abs tol 1e-4
1352
+ array([[0.2, 0.4, 0.5],
1353
+ [0.1, 0.3, 1. ]])
1354
+
1355
+ Multidimensional inputs can be given as well::
1356
+
1357
+ sage: multidim_arr = [[[0, 0.2, 0.4], [0, 1, 0]], [[0, 0, 0], [0.5, 0.6, 0.9]]]
1358
+ sage: hls_to_rgb(multidim_arr) # abs tol 1e-4
1359
+ array([[[0.28, 0.12, 0.12],
1360
+ [1. , 1. , 1. ]],
1361
+ [[0. , 0. , 0. ],
1362
+ [0.24, 0.96, 0.96]]])
1363
+ """
1364
+ import numpy as np
1365
+ hls = np.asarray(hls)
1366
+ if hls.shape[-1] != 3:
1367
+ raise ValueError("Last dimension of input array must be 3; "
1368
+ "shape {} was found.".format(hls.shape))
1369
+ in_shape = hls.shape
1370
+ hls = np.array(
1371
+ hls, copy=False, dtype=np.dtype(float), ndmin=2
1372
+ )
1373
+ rgb = np.zeros_like(hls)
1374
+
1375
+ szero_idx = hls[..., 2] == 0
1376
+ rgb[szero_idx, 0] = hls[szero_idx, 1]
1377
+ rgb[szero_idx, 1] = hls[szero_idx, 1]
1378
+ rgb[szero_idx, 2] = hls[szero_idx, 1]
1379
+
1380
+ snonzero_idx = hls[..., 2] > 0
1381
+ m1 = np.zeros_like(snonzero_idx, dtype=float)
1382
+ m2 = np.zeros_like(snonzero_idx, dtype=float)
1383
+
1384
+ idx = (hls[..., 1] <= 0.5)
1385
+ m2[idx] = hls[idx, 1] * (1.0 + hls[idx, 2])
1386
+
1387
+ idx = (hls[..., 1] > 0.5)
1388
+ m2[idx] = hls[idx, 1] + hls[idx, 2] - hls[idx, 1] * hls[idx, 2]
1389
+
1390
+ m1 = 2 * hls[..., 1] - m2
1391
+
1392
+ rgb[snonzero_idx, 0] = _v(m1, m2, hls[..., 0] + 1.0 / 3.0)[snonzero_idx]
1393
+ rgb[snonzero_idx, 1] = _v(m1, m2, hls[..., 0])[snonzero_idx]
1394
+ rgb[snonzero_idx, 2] = _v(m1, m2, hls[..., 0] - 1.0 / 3.0)[snonzero_idx]
1395
+
1396
+ return rgb.reshape(in_shape)
1397
+
1398
+
1399
+ def _v(m1, m2, hue):
1400
+ """
1401
+ A helper function to convert from hls to rgb.
1402
+
1403
+ This is a numpy version of colorsys._v.
1404
+
1405
+ INPUT:
1406
+
1407
+ - ``m1`` -- an array of floats with values in the range `[0, 1]`
1408
+
1409
+ - ``m2`` -- an array of floats with values in the range `[0, 1]` with
1410
+ the same dimensions as ``m1``
1411
+
1412
+ - ``hue`` -- an array of floats with values in the range `[0, 1]` with
1413
+ the same dimensions as ``m1``
1414
+
1415
+ OUTPUT:
1416
+
1417
+ A Numpy array of floats with values in the range `[0, 1]`. The dimensions
1418
+ are the same as the input arrays.
1419
+
1420
+ EXAMPLES::
1421
+
1422
+ sage: from sage.plot.complex_plot import _v
1423
+ sage: _v([0.1, 0.2], [0.1, 0.3], [0.25, 0.75]) # abs tol 1e-4
1424
+ array([0.1, 0.2])
1425
+ """
1426
+ import numpy as np
1427
+ m1 = np.asarray(m1, dtype=float)
1428
+ m2 = np.asarray(m2, dtype=float)
1429
+ hue = np.asarray(hue, dtype=float)
1430
+
1431
+ if not m1.shape == m2.shape and m1.shape == hue.shape:
1432
+ raise ValueError(
1433
+ "Incompatible shapes given to _v. "
1434
+ "shapes {}, {}, {} given.".format(m1.shape, m2.shape, hue.shape)
1435
+ )
1436
+
1437
+ out = np.zeros_like(m1, dtype=float)
1438
+ hue = hue % 1.0
1439
+
1440
+ conditions = [hue < 1.0 / 6.0, hue < 1.0 / 2.0, hue < 2.0 / 3.0]
1441
+ out = np.select(conditions, [
1442
+ m1 + (m2 - m1) * hue * 6.0,
1443
+ m2,
1444
+ m1 + (m2 - m1) * (2.0 / 3.0 - hue) * 6.0
1445
+ ], default=m1)
1446
+ return out