passagemath-plot 10.6.31rc3__cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.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.
- passagemath_plot-10.6.31rc3.dist-info/METADATA +172 -0
- passagemath_plot-10.6.31rc3.dist-info/RECORD +81 -0
- passagemath_plot-10.6.31rc3.dist-info/WHEEL +6 -0
- passagemath_plot-10.6.31rc3.dist-info/top_level.txt +2 -0
- passagemath_plot.libs/libgfortran-e1b7dfc8.so.5.0.0 +0 -0
- passagemath_plot.libs/libgsl-e3525837.so.28.0.0 +0 -0
- passagemath_plot.libs/libopenblasp-r0-4c5b64b1.3.29.so +0 -0
- sage/all__sagemath_plot.py +15 -0
- sage/ext_data/threejs/animation.css +195 -0
- sage/ext_data/threejs/animation.html +85 -0
- sage/ext_data/threejs/animation.js +273 -0
- sage/ext_data/threejs/fat_lines.js +48 -0
- sage/ext_data/threejs/threejs-version.txt +1 -0
- sage/ext_data/threejs/threejs_template.html +597 -0
- sage/interfaces/all__sagemath_plot.py +1 -0
- sage/interfaces/gnuplot.py +196 -0
- sage/interfaces/jmoldata.py +208 -0
- sage/interfaces/povray.py +56 -0
- sage/plot/all.py +42 -0
- sage/plot/animate.py +1796 -0
- sage/plot/arc.py +504 -0
- sage/plot/arrow.py +671 -0
- sage/plot/bar_chart.py +205 -0
- sage/plot/bezier_path.py +400 -0
- sage/plot/circle.py +435 -0
- sage/plot/colors.py +1606 -0
- sage/plot/complex_plot.cpython-314-aarch64-linux-gnu.so +0 -0
- sage/plot/complex_plot.pyx +1446 -0
- sage/plot/contour_plot.py +1792 -0
- sage/plot/density_plot.py +318 -0
- sage/plot/disk.py +373 -0
- sage/plot/ellipse.py +375 -0
- sage/plot/graphics.py +3580 -0
- sage/plot/histogram.py +354 -0
- sage/plot/hyperbolic_arc.py +404 -0
- sage/plot/hyperbolic_polygon.py +416 -0
- sage/plot/hyperbolic_regular_polygon.py +296 -0
- sage/plot/line.py +626 -0
- sage/plot/matrix_plot.py +629 -0
- sage/plot/misc.py +509 -0
- sage/plot/multigraphics.py +1294 -0
- sage/plot/plot.py +4183 -0
- sage/plot/plot3d/all.py +23 -0
- sage/plot/plot3d/base.cpython-314-aarch64-linux-gnu.so +0 -0
- sage/plot/plot3d/base.pxd +12 -0
- sage/plot/plot3d/base.pyx +3378 -0
- sage/plot/plot3d/implicit_plot3d.py +659 -0
- sage/plot/plot3d/implicit_surface.cpython-314-aarch64-linux-gnu.so +0 -0
- sage/plot/plot3d/implicit_surface.pyx +1453 -0
- sage/plot/plot3d/index_face_set.cpython-314-aarch64-linux-gnu.so +0 -0
- sage/plot/plot3d/index_face_set.pxd +32 -0
- sage/plot/plot3d/index_face_set.pyx +1873 -0
- sage/plot/plot3d/introduction.py +131 -0
- sage/plot/plot3d/list_plot3d.py +649 -0
- sage/plot/plot3d/parametric_plot3d.py +1130 -0
- sage/plot/plot3d/parametric_surface.cpython-314-aarch64-linux-gnu.so +0 -0
- sage/plot/plot3d/parametric_surface.pxd +12 -0
- sage/plot/plot3d/parametric_surface.pyx +893 -0
- sage/plot/plot3d/platonic.py +601 -0
- sage/plot/plot3d/plot3d.py +1442 -0
- sage/plot/plot3d/plot_field3d.py +162 -0
- sage/plot/plot3d/point_c.pxi +148 -0
- sage/plot/plot3d/revolution_plot3d.py +309 -0
- sage/plot/plot3d/shapes.cpython-314-aarch64-linux-gnu.so +0 -0
- sage/plot/plot3d/shapes.pxd +22 -0
- sage/plot/plot3d/shapes.pyx +1382 -0
- sage/plot/plot3d/shapes2.py +1512 -0
- sage/plot/plot3d/tachyon.py +1779 -0
- sage/plot/plot3d/texture.py +453 -0
- sage/plot/plot3d/transform.cpython-314-aarch64-linux-gnu.so +0 -0
- sage/plot/plot3d/transform.pxd +21 -0
- sage/plot/plot3d/transform.pyx +268 -0
- sage/plot/plot3d/tri_plot.py +589 -0
- sage/plot/plot_field.py +362 -0
- sage/plot/point.py +624 -0
- sage/plot/polygon.py +562 -0
- sage/plot/primitive.py +249 -0
- sage/plot/scatter_plot.py +199 -0
- sage/plot/step.py +85 -0
- sage/plot/streamline_plot.py +328 -0
- 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
|