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 http://www.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 >>> 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 = dict(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 += "" 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