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
|