-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathphase_mag_plot.spyx
More file actions
425 lines (319 loc) · 13.7 KB
/
phase_mag_plot.spyx
File metadata and controls
425 lines (319 loc) · 13.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
"""
phase_mag_plot.spyx
This is a modification of sagemath's complex_plot that allows contour-like
plots for complex functions.
AUTHORS:
- David Lowry-Duda (2020-01-01)
"""
# ****************************************************************************
# Copyright (C) 2020 David Lowry-Duda <david@lowryduda.com>
#
# Based (closely) on complex plotting routines from sagemath, written
# by a long list of developers and originally by Robert Bradshaw.
# See sagemath.org for more.
#
# Copyright (C) 2009 Robert Bradshaw <robertwb@math.washington.edu>,
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# https://www.gnu.org/licenses/
# ****************************************************************************
from cysignals.signals cimport sig_on, sig_off, sig_check
cimport numpy as cnumpy
from sage.plot.primitive import GraphicPrimitive
from sage.misc.decorators import options
from sage.rings.complex_double cimport ComplexDoubleElement
from sage.arith.srange import srange
from libc.math cimport hypot, atan2, atan, log, log2, sqrt
from sage.arith.constants cimport M_PI as PI
cdef inline ComplexDoubleElement new_CDF_element(double x, double y):
z = <ComplexDoubleElement>ComplexDoubleElement.__new__(ComplexDoubleElement)
z._complex.real = x
z._complex.imag = y
return z
cdef inline double cyclic_mag_to_lightness(double r):
"""
This affects how the magnitude affects the color. Tweaking this will affect
how the magnitude affects the color. Changing `log2` to another function
will cause many changes. For example, removing the `log2` will give linear
cyclic growths instead of logarithmic cycles, and may be more appropriate
for complex functions that have little variation in size. Anything near
zero will be darker and large values will be lighter.
INPUT:
- ``r`` - a non-negative real number
OUTPUT:
A value between `-1` (black) and `+1` (white), inclusive.
"""
# if r <= 0.0001:
# return (.5 - r / 8.) - 0.35
rem = log2(r) % 1
if rem < 0:
rem += 1
return .15 - rem/2.
cdef double mag_and_arg_to_lightness(double r, double arg):
r_rem = log2(r) % 1
if r_rem < 0:
r_rem += 1
a_rem = 5 * arg / PI % 1
if a_rem < 0:
a_rem += 1
return 0.15 - (r_rem)/3. - (a_rem)/3.
def complex_to_rgb(z_values, tiled=False):
"""
INPUT:
- ``z_values`` -- A grid of complex numbers, as a list of lists
OUTPUT:
An `N \\times M \\times 3` floating point Numpy array ``X``, where
``X[i,j]`` is an (r,g,b) tuple.
EXAMPLES::
sage: from sage.plot.complex_plot import complex_to_rgb
sage: complex_to_rgb([[0, 1, 1000]])
array([[[0. , 0. , 0. ],
[0.77172568, 0. , 0. ],
[1. , 0.64421177, 0.64421177]]])
sage: complex_to_rgb([[0, 1j, 1000j]])
array([[[0. , 0. , 0. ],
[0.38586284, 0.77172568, 0. ],
[0.82210588, 1. , 0.64421177]]])
"""
import numpy
cdef unsigned int i, j, imax, jmax
cdef double x, y, mag, arg
cdef double lightness, hue, top, bot
cdef double r, g, b
cdef int ihue
cdef ComplexDoubleElement z
from sage.rings.complex_double import CDF
imax = len(z_values)
jmax = len(z_values[0])
cdef cnumpy.ndarray[cnumpy.float_t, ndim=3, mode='c'] rgb = numpy.empty(dtype=numpy.float, shape=(imax, jmax, 3))
sig_on()
for i from 0 <= i < imax:
row = z_values[i]
for j from 0 <= j < jmax:
zz = row[j]
if type(zz) is ComplexDoubleElement:
z = <ComplexDoubleElement>zz
else:
z = CDF(zz)
x, y = z._complex.dat
mag = hypot(x, y)
arg = atan2(y, x) # math module arctan has range from -pi to pi, so cut along negative x-axis
if tiled:
lightness = mag_and_arg_to_lightness(mag, arg)
else:
lightness = cyclic_mag_to_lightness(mag)
if lightness < 0: # in hsv, variable value, full saturation (s=1, v=1+lightness)
bot = 0
top = (1+lightness)
else: # in hsv, variable saturation, full value (v=1, s=1-lightness)
bot = lightness
top = 1
hue = 3*arg/PI # Note that does same thing as colorsys module hsv_to_rgb for this setup, but in Cython
if hue < 0: hue += 6 # usual hsv hue is thus h=arg/(2*pi) for positive, h=arg/(2*PI)+1 for negative
ihue = <int>hue
if ihue == 0:
r = top
g = bot + hue * (top-bot)
b = bot
elif ihue == 1:
r = bot + (2-hue) * (top-bot)
g = top
b = bot
elif ihue == 2:
r = bot
g = top
b = bot + (hue-2) * (top-bot)
elif ihue == 3:
r = bot
g = bot + (4-hue) * (top-bot)
b = top
elif ihue == 4:
r = bot + (hue-4) * (top-bot)
g = bot
b = top
else:
r = top
g = bot
b = bot + (6-hue) * (top-bot)
rgb[i, j, 0] = r
rgb[i, j, 1] = g
rgb[i, j, 2] = b
sig_off()
return rgb
class ComplexPlot(GraphicPrimitive):
"""
The GraphicsPrimitive to display complex functions in using the domain
coloring method
INPUT:
- ``rgb_data`` -- An array of colored points to be plotted.
- ``x_range`` -- A minimum and maximum x value for the plot.
- ``y_range`` -- A minimum and maximum y value for the plot.
TESTS::
sage: p = complex_plot(lambda z: z^2-1, (-2, 2), (-2, 2))
"""
def __init__(self, rgb_data, x_range, y_range, options):
"""
TESTS::
sage: p = complex_plot(lambda z: z^2-1, (-2, 2), (-2, 2))
"""
self.x_range = x_range
self.y_range = y_range
self.x_count = len(rgb_data)
self.y_count = len(rgb_data[0])
self.rgb_data = rgb_data
GraphicPrimitive.__init__(self, options)
def get_minmax_data(self):
"""
Return a dictionary with the bounding box data.
EXAMPLES::
sage: p = complex_plot(lambda z: z, (-1, 2), (-3, 4))
sage: sorted(p.get_minmax_data().items())
[('xmax', 2.0), ('xmin', -1.0), ('ymax', 4.0), ('ymin', -3.0)]
sage: p = complex_plot(lambda z: z, (1, 2), (3, 4))
sage: sorted(p.get_minmax_data().items())
[('xmax', 2.0), ('xmin', 1.0), ('ymax', 4.0), ('ymin', 3.0)]
"""
from sage.plot.plot import minmax_data
return minmax_data(self.x_range, self.y_range, dict=True)
def _allowed_options(self):
"""
TESTS::
sage: isinstance(complex_plot(lambda z: z, (-1,1), (-1,1))[0]._allowed_options(), dict)
True
"""
return {'plot_points':'How many points to use for plotting precision',
'interpolation':'What interpolation method to use'}
def _repr_(self):
"""
TESTS::
sage: isinstance(complex_plot(lambda z: z, (-1,1), (-1,1))[0]._repr_(), str)
True
"""
return "ComplexPlot defined by a %s x %s data grid"%(self.x_count, self.y_count)
def _render_on_subplot(self, subplot):
"""
TESTS::
sage: complex_plot(lambda x: x^2, (-5, 5), (-5, 5))
Graphics object consisting of 1 graphics primitive
"""
options = self.options()
x0, x1 = float(self.x_range[0]), float(self.x_range[1])
y0, y1 = float(self.y_range[0]), float(self.y_range[1])
subplot.imshow(self.rgb_data, origin='lower', extent=(x0, x1, y0, y1),
interpolation=options['interpolation'])
@options(plot_points=100, interpolation='catrom')
def phase_mag_complex_plot(f, x_range, y_range, tiled=False, **options):
r"""
``phase_mag_complex_plot`` takes a complex function of one variable,
`f(z)` and plots output of the function over the specified
``x_range`` and ``y_range`` as demonstrated below. The magnitude of the
output is indicated by the brightness (with zero being black and
infinity being white) while the argument is represented by the
hue (with red being positive real, and increasing through orange,
yellow, ... as the argument increases).
``complex_plot(f, (xmin, xmax), (ymin, ymax), ...)``
INPUT:
- ``f`` -- a function of a single complex value `x + iy`
- ``(xmin, xmax)`` -- 2-tuple, the range of ``x`` values
- ``(ymin, ymax)`` -- 2-tuple, the range of ``y`` values
The following inputs must all be passed in as named parameters:
- ``plot_points`` -- integer (default: 100); number of points to plot
in each direction of the grid
- ``interpolation`` -- string (default: ``'catrom'``), the interpolation
method to use: ``'bilinear'``, ``'bicubic'``, ``'spline16'``,
``'spline36'``, ``'quadric'``, ``'gaussian'``, ``'sinc'``,
``'bessel'``, ``'mitchell'``, ``'lanczos'``, ``'catrom'``,
``'hermite'``, ``'hanning'``, ``'hamming'``, ``'kaiser'``
EXAMPLES:
Here we plot a couple of simple functions::
sage: phase_mag_complex_plot(sqrt(x), (-5, 5), (-5, 5))
Graphics object consisting of 1 graphics primitive
.. PLOT::
sphinx_plot(phase_mag_complex_plot(sqrt(x), (-5, 5), (-5, 5)))
::
sage: phase_mag_complex_plot(sin(x), (-5, 5), (-5, 5))
Graphics object consisting of 1 graphics primitive
.. PLOT::
sphinx_plot(phase_mag_complex_plot(sin(x), (-5, 5), (-5, 5)))
::
sage: phase_mag_complex_plot(log(x), (-10, 10), (-10, 10))
Graphics object consisting of 1 graphics primitive
.. PLOT::
sphinx_plot(phase_mag_complex_plot(log(x), (-10, 10), (-10, 10)))
::
sage: phase_mag_complex_plot(exp(x), (-10, 10), (-10, 10))
Graphics object consisting of 1 graphics primitive
.. PLOT::
sphinx_plot(phase_mag_complex_plot(exp(x), (-10, 10), (-10, 10)))
A function with some nice zeros and a pole::
sage: f(z) = z^5 + z - 1 + 1/z
sage: phase_mag_complex_plot(f, (-3, 3), (-3, 3))
Graphics object consisting of 1 graphics primitive
.. PLOT::
def f(z): return z**5 + z - 1 + 1/z
sphinx_plot(phase_mag_complex_plot(f, (-3, 3), (-3, 3)))
Here is the identity, useful for seeing what values map to what colors::
sage: phase_mag_complex_plot(lambda z: z, (-3, 3), (-3, 3))
Graphics object consisting of 1 graphics primitive
.. PLOT::
sphinx_plot(phase_mag_complex_plot(lambda z: z, (-3, 3), (-3, 3)))
The Riemann Zeta function::
sage: phase_mag_complex_plot(zeta, (-30,30), (-30,30))
Graphics object consisting of 1 graphics primitive
.. PLOT::
sphinx_plot(phase_mag_complex_plot(zeta, (-30,30), (-30,30)))
Extra options will get passed on to show(), as long as they are valid::
sage: phase_mag_complex_plot(lambda z: z, (-3, 3), (-3, 3), figsize=[1,1])
Graphics object consisting of 1 graphics primitive
::
sage: phase_mag_complex_plot(lambda z: z, (-3, 3), (-3, 3)).show(figsize=[1,1]) # These are equivalent
TESTS:
Test to make sure that using fast_callable functions works::
sage: f(x) = x^2
sage: g = fast_callable(f, domain=CC, vars='x')
sage: h = fast_callable(f, domain=CDF, vars='x')
sage: P = phase_mag_complex_plot(f, (-10, 10), (-10, 10))
sage: Q = phase_mag_complex_plot(g, (-10, 10), (-10, 10))
sage: R = phase_mag_complex_plot(h, (-10, 10), (-10, 10))
sage: S = phase_mag_complex_plot(exp(x)-sin(x), (-10, 10), (-10, 10))
sage: P; Q; R; S
Graphics object consisting of 1 graphics primitive
Graphics object consisting of 1 graphics primitive
Graphics object consisting of 1 graphics primitive
Graphics object consisting of 1 graphics primitive
Test to make sure symbolic functions still work without declaring
a variable. (We don't do this in practice because it doesn't use
fast_callable, so it is much slower.)
::
sage: phase_mag_complex_plot(sqrt, (-5, 5), (-5, 5))
Graphics object consisting of 1 graphics primitive
"""
from sage.plot.all import Graphics
from sage.plot.misc import setup_for_eval_on_grid
from sage.ext.fast_callable import fast_callable
from sage.rings.complex_double import CDF
try:
f = fast_callable(f, domain=CDF, expect_one_var=True)
except (AttributeError, TypeError, ValueError):
pass
cdef double x, y
_, ranges = setup_for_eval_on_grid([], [x_range, y_range],
options['plot_points'])
x_range = ranges[0]
y_range = ranges[1]
cdef list z_values = []
cdef list row
for y in srange(*y_range, include_endpoint=True):
row = []
for x in srange(*x_range, include_endpoint=True):
sig_check()
row.append(f(new_CDF_element(x, y)))
z_values.append(row)
g = Graphics()
g._set_extra_kwds(Graphics._extract_kwds_for_show(options, ignore=['xmin', 'xmax']))
g.add_primitive(ComplexPlot(complex_to_rgb(z_values, tiled=tiled),
x_range[:2], y_range[:2], options))
return g