465 lines
13 KiB
Python
465 lines
13 KiB
Python
from threading import RLock
|
|
|
|
# it is sufficient to import "pyglet" here once
|
|
try:
|
|
import pyglet.gl as pgl
|
|
except ImportError:
|
|
raise ImportError("pyglet is required for plotting.\n "
|
|
"visit https://pyglet.org/")
|
|
|
|
from sympy.core.numbers import Integer
|
|
from sympy.external.gmpy import SYMPY_INTS
|
|
from sympy.geometry.entity import GeometryEntity
|
|
from sympy.plotting.pygletplot.plot_axes import PlotAxes
|
|
from sympy.plotting.pygletplot.plot_mode import PlotMode
|
|
from sympy.plotting.pygletplot.plot_object import PlotObject
|
|
from sympy.plotting.pygletplot.plot_window import PlotWindow
|
|
from sympy.plotting.pygletplot.util import parse_option_string
|
|
from sympy.utilities.decorator import doctest_depends_on
|
|
from sympy.utilities.iterables import is_sequence
|
|
|
|
from time import sleep
|
|
from os import getcwd, listdir
|
|
|
|
import ctypes
|
|
|
|
@doctest_depends_on(modules=('pyglet',))
|
|
class PygletPlot:
|
|
"""
|
|
Plot Examples
|
|
=============
|
|
|
|
See examples/advanced/pyglet_plotting.py for many more examples.
|
|
|
|
>>> from sympy.plotting.pygletplot import PygletPlot as Plot
|
|
>>> from sympy.abc import x, y, z
|
|
|
|
>>> Plot(x*y**3-y*x**3)
|
|
[0]: -x**3*y + x*y**3, 'mode=cartesian'
|
|
|
|
>>> p = Plot()
|
|
>>> p[1] = x*y
|
|
>>> p[1].color = z, (0.4,0.4,0.9), (0.9,0.4,0.4)
|
|
|
|
>>> p = Plot()
|
|
>>> p[1] = x**2+y**2
|
|
>>> p[2] = -x**2-y**2
|
|
|
|
|
|
Variable Intervals
|
|
==================
|
|
|
|
The basic format is [var, min, max, steps], but the
|
|
syntax is flexible and arguments left out are taken
|
|
from the defaults for the current coordinate mode:
|
|
|
|
>>> Plot(x**2) # implies [x,-5,5,100]
|
|
[0]: x**2, 'mode=cartesian'
|
|
>>> Plot(x**2, [], []) # [x,-1,1,40], [y,-1,1,40]
|
|
[0]: x**2, 'mode=cartesian'
|
|
>>> Plot(x**2-y**2, [100], [100]) # [x,-1,1,100], [y,-1,1,100]
|
|
[0]: x**2 - y**2, 'mode=cartesian'
|
|
>>> Plot(x**2, [x,-13,13,100])
|
|
[0]: x**2, 'mode=cartesian'
|
|
>>> Plot(x**2, [-13,13]) # [x,-13,13,100]
|
|
[0]: x**2, 'mode=cartesian'
|
|
>>> Plot(x**2, [x,-13,13]) # [x,-13,13,10]
|
|
[0]: x**2, 'mode=cartesian'
|
|
>>> Plot(1*x, [], [x], mode='cylindrical')
|
|
... # [unbound_theta,0,2*Pi,40], [x,-1,1,20]
|
|
[0]: x, 'mode=cartesian'
|
|
|
|
|
|
Coordinate Modes
|
|
================
|
|
|
|
Plot supports several curvilinear coordinate modes, and
|
|
they independent for each plotted function. You can specify
|
|
a coordinate mode explicitly with the 'mode' named argument,
|
|
but it can be automatically determined for Cartesian or
|
|
parametric plots, and therefore must only be specified for
|
|
polar, cylindrical, and spherical modes.
|
|
|
|
Specifically, Plot(function arguments) and Plot[n] =
|
|
(function arguments) will interpret your arguments as a
|
|
Cartesian plot if you provide one function and a parametric
|
|
plot if you provide two or three functions. Similarly, the
|
|
arguments will be interpreted as a curve if one variable is
|
|
used, and a surface if two are used.
|
|
|
|
Supported mode names by number of variables:
|
|
|
|
1: parametric, cartesian, polar
|
|
2: parametric, cartesian, cylindrical = polar, spherical
|
|
|
|
>>> Plot(1, mode='spherical')
|
|
|
|
|
|
Calculator-like Interface
|
|
=========================
|
|
|
|
>>> p = Plot(visible=False)
|
|
>>> f = x**2
|
|
>>> p[1] = f
|
|
>>> p[2] = f.diff(x)
|
|
>>> p[3] = f.diff(x).diff(x)
|
|
>>> p
|
|
[1]: x**2, 'mode=cartesian'
|
|
[2]: 2*x, 'mode=cartesian'
|
|
[3]: 2, 'mode=cartesian'
|
|
>>> p.show()
|
|
>>> p.clear()
|
|
>>> p
|
|
<blank plot>
|
|
>>> p[1] = x**2+y**2
|
|
>>> p[1].style = 'solid'
|
|
>>> p[2] = -x**2-y**2
|
|
>>> p[2].style = 'wireframe'
|
|
>>> p[1].color = z, (0.4,0.4,0.9), (0.9,0.4,0.4)
|
|
>>> p[1].style = 'both'
|
|
>>> p[2].style = 'both'
|
|
>>> p.close()
|
|
|
|
|
|
Plot Window Keyboard Controls
|
|
=============================
|
|
|
|
Screen Rotation:
|
|
X,Y axis Arrow Keys, A,S,D,W, Numpad 4,6,8,2
|
|
Z axis Q,E, Numpad 7,9
|
|
|
|
Model Rotation:
|
|
Z axis Z,C, Numpad 1,3
|
|
|
|
Zoom: R,F, PgUp,PgDn, Numpad +,-
|
|
|
|
Reset Camera: X, Numpad 5
|
|
|
|
Camera Presets:
|
|
XY F1
|
|
XZ F2
|
|
YZ F3
|
|
Perspective F4
|
|
|
|
Sensitivity Modifier: SHIFT
|
|
|
|
Axes Toggle:
|
|
Visible F5
|
|
Colors F6
|
|
|
|
Close Window: ESCAPE
|
|
|
|
=============================
|
|
|
|
"""
|
|
|
|
@doctest_depends_on(modules=('pyglet',))
|
|
def __init__(self, *fargs, **win_args):
|
|
"""
|
|
Positional Arguments
|
|
====================
|
|
|
|
Any given positional arguments are used to
|
|
initialize a plot function at index 1. In
|
|
other words...
|
|
|
|
>>> from sympy.plotting.pygletplot import PygletPlot as Plot
|
|
>>> from sympy.abc import x
|
|
>>> p = Plot(x**2, visible=False)
|
|
|
|
...is equivalent to...
|
|
|
|
>>> p = Plot(visible=False)
|
|
>>> p[1] = x**2
|
|
|
|
Note that in earlier versions of the plotting
|
|
module, you were able to specify multiple
|
|
functions in the initializer. This functionality
|
|
has been dropped in favor of better automatic
|
|
plot plot_mode detection.
|
|
|
|
|
|
Named Arguments
|
|
===============
|
|
|
|
axes
|
|
An option string of the form
|
|
"key1=value1; key2 = value2" which
|
|
can use the following options:
|
|
|
|
style = ordinate
|
|
none OR frame OR box OR ordinate
|
|
|
|
stride = 0.25
|
|
val OR (val_x, val_y, val_z)
|
|
|
|
overlay = True (draw on top of plot)
|
|
True OR False
|
|
|
|
colored = False (False uses Black,
|
|
True uses colors
|
|
R,G,B = X,Y,Z)
|
|
True OR False
|
|
|
|
label_axes = False (display axis names
|
|
at endpoints)
|
|
True OR False
|
|
|
|
visible = True (show immediately
|
|
True OR False
|
|
|
|
|
|
The following named arguments are passed as
|
|
arguments to window initialization:
|
|
|
|
antialiasing = True
|
|
True OR False
|
|
|
|
ortho = False
|
|
True OR False
|
|
|
|
invert_mouse_zoom = False
|
|
True OR False
|
|
|
|
"""
|
|
# Register the plot modes
|
|
from . import plot_modes # noqa
|
|
|
|
self._win_args = win_args
|
|
self._window = None
|
|
|
|
self._render_lock = RLock()
|
|
|
|
self._functions = {}
|
|
self._pobjects = []
|
|
self._screenshot = ScreenShot(self)
|
|
|
|
axe_options = parse_option_string(win_args.pop('axes', ''))
|
|
self.axes = PlotAxes(**axe_options)
|
|
self._pobjects.append(self.axes)
|
|
|
|
self[0] = fargs
|
|
if win_args.get('visible', True):
|
|
self.show()
|
|
|
|
## Window Interfaces
|
|
|
|
def show(self):
|
|
"""
|
|
Creates and displays a plot window, or activates it
|
|
(gives it focus) if it has already been created.
|
|
"""
|
|
if self._window and not self._window.has_exit:
|
|
self._window.activate()
|
|
else:
|
|
self._win_args['visible'] = True
|
|
self.axes.reset_resources()
|
|
|
|
#if hasattr(self, '_doctest_depends_on'):
|
|
# self._win_args['runfromdoctester'] = True
|
|
|
|
self._window = PlotWindow(self, **self._win_args)
|
|
|
|
def close(self):
|
|
"""
|
|
Closes the plot window.
|
|
"""
|
|
if self._window:
|
|
self._window.close()
|
|
|
|
def saveimage(self, outfile=None, format='', size=(600, 500)):
|
|
"""
|
|
Saves a screen capture of the plot window to an
|
|
image file.
|
|
|
|
If outfile is given, it can either be a path
|
|
or a file object. Otherwise a png image will
|
|
be saved to the current working directory.
|
|
If the format is omitted, it is determined from
|
|
the filename extension.
|
|
"""
|
|
self._screenshot.save(outfile, format, size)
|
|
|
|
## Function List Interfaces
|
|
|
|
def clear(self):
|
|
"""
|
|
Clears the function list of this plot.
|
|
"""
|
|
self._render_lock.acquire()
|
|
self._functions = {}
|
|
self.adjust_all_bounds()
|
|
self._render_lock.release()
|
|
|
|
def __getitem__(self, i):
|
|
"""
|
|
Returns the function at position i in the
|
|
function list.
|
|
"""
|
|
return self._functions[i]
|
|
|
|
def __setitem__(self, i, args):
|
|
"""
|
|
Parses and adds a PlotMode to the function
|
|
list.
|
|
"""
|
|
if not (isinstance(i, (SYMPY_INTS, Integer)) and i >= 0):
|
|
raise ValueError("Function index must "
|
|
"be an integer >= 0.")
|
|
|
|
if isinstance(args, PlotObject):
|
|
f = args
|
|
else:
|
|
if (not is_sequence(args)) or isinstance(args, GeometryEntity):
|
|
args = [args]
|
|
if len(args) == 0:
|
|
return # no arguments given
|
|
kwargs = {"bounds_callback": self.adjust_all_bounds}
|
|
f = PlotMode(*args, **kwargs)
|
|
|
|
if f:
|
|
self._render_lock.acquire()
|
|
self._functions[i] = f
|
|
self._render_lock.release()
|
|
else:
|
|
raise ValueError("Failed to parse '%s'."
|
|
% ', '.join(str(a) for a in args))
|
|
|
|
def __delitem__(self, i):
|
|
"""
|
|
Removes the function in the function list at
|
|
position i.
|
|
"""
|
|
self._render_lock.acquire()
|
|
del self._functions[i]
|
|
self.adjust_all_bounds()
|
|
self._render_lock.release()
|
|
|
|
def firstavailableindex(self):
|
|
"""
|
|
Returns the first unused index in the function list.
|
|
"""
|
|
i = 0
|
|
self._render_lock.acquire()
|
|
while i in self._functions:
|
|
i += 1
|
|
self._render_lock.release()
|
|
return i
|
|
|
|
def append(self, *args):
|
|
"""
|
|
Parses and adds a PlotMode to the function
|
|
list at the first available index.
|
|
"""
|
|
self.__setitem__(self.firstavailableindex(), args)
|
|
|
|
def __len__(self):
|
|
"""
|
|
Returns the number of functions in the function list.
|
|
"""
|
|
return len(self._functions)
|
|
|
|
def __iter__(self):
|
|
"""
|
|
Allows iteration of the function list.
|
|
"""
|
|
return self._functions.itervalues()
|
|
|
|
def __repr__(self):
|
|
return str(self)
|
|
|
|
def __str__(self):
|
|
"""
|
|
Returns a string containing a new-line separated
|
|
list of the functions in the function list.
|
|
"""
|
|
s = ""
|
|
if len(self._functions) == 0:
|
|
s += "<blank plot>"
|
|
else:
|
|
self._render_lock.acquire()
|
|
s += "\n".join(["%s[%i]: %s" % ("", i, str(self._functions[i]))
|
|
for i in self._functions])
|
|
self._render_lock.release()
|
|
return s
|
|
|
|
def adjust_all_bounds(self):
|
|
self._render_lock.acquire()
|
|
self.axes.reset_bounding_box()
|
|
for f in self._functions:
|
|
self.axes.adjust_bounds(self._functions[f].bounds)
|
|
self._render_lock.release()
|
|
|
|
def wait_for_calculations(self):
|
|
sleep(0)
|
|
self._render_lock.acquire()
|
|
for f in self._functions:
|
|
a = self._functions[f]._get_calculating_verts
|
|
b = self._functions[f]._get_calculating_cverts
|
|
while a() or b():
|
|
sleep(0)
|
|
self._render_lock.release()
|
|
|
|
class ScreenShot:
|
|
def __init__(self, plot):
|
|
self._plot = plot
|
|
self.screenshot_requested = False
|
|
self.outfile = None
|
|
self.format = ''
|
|
self.invisibleMode = False
|
|
self.flag = 0
|
|
|
|
def __bool__(self):
|
|
return self.screenshot_requested
|
|
|
|
def _execute_saving(self):
|
|
if self.flag < 3:
|
|
self.flag += 1
|
|
return
|
|
|
|
size_x, size_y = self._plot._window.get_size()
|
|
size = size_x*size_y*4*ctypes.sizeof(ctypes.c_ubyte)
|
|
image = ctypes.create_string_buffer(size)
|
|
pgl.glReadPixels(0, 0, size_x, size_y, pgl.GL_RGBA, pgl.GL_UNSIGNED_BYTE, image)
|
|
from PIL import Image
|
|
im = Image.frombuffer('RGBA', (size_x, size_y),
|
|
image.raw, 'raw', 'RGBA', 0, 1)
|
|
im.transpose(Image.FLIP_TOP_BOTTOM).save(self.outfile, self.format)
|
|
|
|
self.flag = 0
|
|
self.screenshot_requested = False
|
|
if self.invisibleMode:
|
|
self._plot._window.close()
|
|
|
|
def save(self, outfile=None, format='', size=(600, 500)):
|
|
self.outfile = outfile
|
|
self.format = format
|
|
self.size = size
|
|
self.screenshot_requested = True
|
|
|
|
if not self._plot._window or self._plot._window.has_exit:
|
|
self._plot._win_args['visible'] = False
|
|
|
|
self._plot._win_args['width'] = size[0]
|
|
self._plot._win_args['height'] = size[1]
|
|
|
|
self._plot.axes.reset_resources()
|
|
self._plot._window = PlotWindow(self._plot, **self._plot._win_args)
|
|
self.invisibleMode = True
|
|
|
|
if self.outfile is None:
|
|
self.outfile = self._create_unique_path()
|
|
print(self.outfile)
|
|
|
|
def _create_unique_path(self):
|
|
cwd = getcwd()
|
|
l = listdir(cwd)
|
|
path = ''
|
|
i = 0
|
|
while True:
|
|
if not 'plot_%s.png' % i in l:
|
|
path = cwd + '/plot_%s.png' % i
|
|
break
|
|
i += 1
|
|
return path
|