I am done

This commit is contained in:
2024-10-30 22:14:35 +01:00
parent 720dc28c09
commit 40e2a747cf
36901 changed files with 5011519 additions and 0 deletions

View File

@ -0,0 +1,138 @@
"""Plotting module that can plot 2D and 3D functions
"""
from sympy.utilities.decorator import doctest_depends_on
@doctest_depends_on(modules=('pyglet',))
def PygletPlot(*args, **kwargs):
"""
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,100]
[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
=============================
"""
from sympy.plotting.pygletplot.plot import PygletPlot
return PygletPlot(*args, **kwargs)

View File

@ -0,0 +1,336 @@
from sympy.core.basic import Basic
from sympy.core.symbol import (Symbol, symbols)
from sympy.utilities.lambdify import lambdify
from .util import interpolate, rinterpolate, create_bounds, update_bounds
from sympy.utilities.iterables import sift
class ColorGradient:
colors = [0.4, 0.4, 0.4], [0.9, 0.9, 0.9]
intervals = 0.0, 1.0
def __init__(self, *args):
if len(args) == 2:
self.colors = list(args)
self.intervals = [0.0, 1.0]
elif len(args) > 0:
if len(args) % 2 != 0:
raise ValueError("len(args) should be even")
self.colors = [args[i] for i in range(1, len(args), 2)]
self.intervals = [args[i] for i in range(0, len(args), 2)]
assert len(self.colors) == len(self.intervals)
def copy(self):
c = ColorGradient()
c.colors = [e[::] for e in self.colors]
c.intervals = self.intervals[::]
return c
def _find_interval(self, v):
m = len(self.intervals)
i = 0
while i < m - 1 and self.intervals[i] <= v:
i += 1
return i
def _interpolate_axis(self, axis, v):
i = self._find_interval(v)
v = rinterpolate(self.intervals[i - 1], self.intervals[i], v)
return interpolate(self.colors[i - 1][axis], self.colors[i][axis], v)
def __call__(self, r, g, b):
c = self._interpolate_axis
return c(0, r), c(1, g), c(2, b)
default_color_schemes = {} # defined at the bottom of this file
class ColorScheme:
def __init__(self, *args, **kwargs):
self.args = args
self.f, self.gradient = None, ColorGradient()
if len(args) == 1 and not isinstance(args[0], Basic) and callable(args[0]):
self.f = args[0]
elif len(args) == 1 and isinstance(args[0], str):
if args[0] in default_color_schemes:
cs = default_color_schemes[args[0]]
self.f, self.gradient = cs.f, cs.gradient.copy()
else:
self.f = lambdify('x,y,z,u,v', args[0])
else:
self.f, self.gradient = self._interpret_args(args)
self._test_color_function()
if not isinstance(self.gradient, ColorGradient):
raise ValueError("Color gradient not properly initialized. "
"(Not a ColorGradient instance.)")
def _interpret_args(self, args):
f, gradient = None, self.gradient
atoms, lists = self._sort_args(args)
s = self._pop_symbol_list(lists)
s = self._fill_in_vars(s)
# prepare the error message for lambdification failure
f_str = ', '.join(str(fa) for fa in atoms)
s_str = (str(sa) for sa in s)
s_str = ', '.join(sa for sa in s_str if sa.find('unbound') < 0)
f_error = ValueError("Could not interpret arguments "
"%s as functions of %s." % (f_str, s_str))
# try to lambdify args
if len(atoms) == 1:
fv = atoms[0]
try:
f = lambdify(s, [fv, fv, fv])
except TypeError:
raise f_error
elif len(atoms) == 3:
fr, fg, fb = atoms
try:
f = lambdify(s, [fr, fg, fb])
except TypeError:
raise f_error
else:
raise ValueError("A ColorScheme must provide 1 or 3 "
"functions in x, y, z, u, and/or v.")
# try to intrepret any given color information
if len(lists) == 0:
gargs = []
elif len(lists) == 1:
gargs = lists[0]
elif len(lists) == 2:
try:
(r1, g1, b1), (r2, g2, b2) = lists
except TypeError:
raise ValueError("If two color arguments are given, "
"they must be given in the format "
"(r1, g1, b1), (r2, g2, b2).")
gargs = lists
elif len(lists) == 3:
try:
(r1, r2), (g1, g2), (b1, b2) = lists
except Exception:
raise ValueError("If three color arguments are given, "
"they must be given in the format "
"(r1, r2), (g1, g2), (b1, b2). To create "
"a multi-step gradient, use the syntax "
"[0, colorStart, step1, color1, ..., 1, "
"colorEnd].")
gargs = [[r1, g1, b1], [r2, g2, b2]]
else:
raise ValueError("Don't know what to do with collection "
"arguments %s." % (', '.join(str(l) for l in lists)))
if gargs:
try:
gradient = ColorGradient(*gargs)
except Exception as ex:
raise ValueError(("Could not initialize a gradient "
"with arguments %s. Inner "
"exception: %s") % (gargs, str(ex)))
return f, gradient
def _pop_symbol_list(self, lists):
symbol_lists = []
for l in lists:
mark = True
for s in l:
if s is not None and not isinstance(s, Symbol):
mark = False
break
if mark:
lists.remove(l)
symbol_lists.append(l)
if len(symbol_lists) == 1:
return symbol_lists[0]
elif len(symbol_lists) == 0:
return []
else:
raise ValueError("Only one list of Symbols "
"can be given for a color scheme.")
def _fill_in_vars(self, args):
defaults = symbols('x,y,z,u,v')
v_error = ValueError("Could not find what to plot.")
if len(args) == 0:
return defaults
if not isinstance(args, (tuple, list)):
raise v_error
if len(args) == 0:
return defaults
for s in args:
if s is not None and not isinstance(s, Symbol):
raise v_error
# when vars are given explicitly, any vars
# not given are marked 'unbound' as to not
# be accidentally used in an expression
vars = [Symbol('unbound%i' % (i)) for i in range(1, 6)]
# interpret as t
if len(args) == 1:
vars[3] = args[0]
# interpret as u,v
elif len(args) == 2:
if args[0] is not None:
vars[3] = args[0]
if args[1] is not None:
vars[4] = args[1]
# interpret as x,y,z
elif len(args) >= 3:
# allow some of x,y,z to be
# left unbound if not given
if args[0] is not None:
vars[0] = args[0]
if args[1] is not None:
vars[1] = args[1]
if args[2] is not None:
vars[2] = args[2]
# interpret the rest as t
if len(args) >= 4:
vars[3] = args[3]
# ...or u,v
if len(args) >= 5:
vars[4] = args[4]
return vars
def _sort_args(self, args):
lists, atoms = sift(args,
lambda a: isinstance(a, (tuple, list)), binary=True)
return atoms, lists
def _test_color_function(self):
if not callable(self.f):
raise ValueError("Color function is not callable.")
try:
result = self.f(0, 0, 0, 0, 0)
if len(result) != 3:
raise ValueError("length should be equal to 3")
except TypeError:
raise ValueError("Color function needs to accept x,y,z,u,v, "
"as arguments even if it doesn't use all of them.")
except AssertionError:
raise ValueError("Color function needs to return 3-tuple r,g,b.")
except Exception:
pass # color function probably not valid at 0,0,0,0,0
def __call__(self, x, y, z, u, v):
try:
return self.f(x, y, z, u, v)
except Exception:
return None
def apply_to_curve(self, verts, u_set, set_len=None, inc_pos=None):
"""
Apply this color scheme to a
set of vertices over a single
independent variable u.
"""
bounds = create_bounds()
cverts = []
if callable(set_len):
set_len(len(u_set)*2)
# calculate f() = r,g,b for each vert
# and find the min and max for r,g,b
for _u in range(len(u_set)):
if verts[_u] is None:
cverts.append(None)
else:
x, y, z = verts[_u]
u, v = u_set[_u], None
c = self(x, y, z, u, v)
if c is not None:
c = list(c)
update_bounds(bounds, c)
cverts.append(c)
if callable(inc_pos):
inc_pos()
# scale and apply gradient
for _u in range(len(u_set)):
if cverts[_u] is not None:
for _c in range(3):
# scale from [f_min, f_max] to [0,1]
cverts[_u][_c] = rinterpolate(bounds[_c][0], bounds[_c][1],
cverts[_u][_c])
# apply gradient
cverts[_u] = self.gradient(*cverts[_u])
if callable(inc_pos):
inc_pos()
return cverts
def apply_to_surface(self, verts, u_set, v_set, set_len=None, inc_pos=None):
"""
Apply this color scheme to a
set of vertices over two
independent variables u and v.
"""
bounds = create_bounds()
cverts = []
if callable(set_len):
set_len(len(u_set)*len(v_set)*2)
# calculate f() = r,g,b for each vert
# and find the min and max for r,g,b
for _u in range(len(u_set)):
column = []
for _v in range(len(v_set)):
if verts[_u][_v] is None:
column.append(None)
else:
x, y, z = verts[_u][_v]
u, v = u_set[_u], v_set[_v]
c = self(x, y, z, u, v)
if c is not None:
c = list(c)
update_bounds(bounds, c)
column.append(c)
if callable(inc_pos):
inc_pos()
cverts.append(column)
# scale and apply gradient
for _u in range(len(u_set)):
for _v in range(len(v_set)):
if cverts[_u][_v] is not None:
# scale from [f_min, f_max] to [0,1]
for _c in range(3):
cverts[_u][_v][_c] = rinterpolate(bounds[_c][0],
bounds[_c][1], cverts[_u][_v][_c])
# apply gradient
cverts[_u][_v] = self.gradient(*cverts[_u][_v])
if callable(inc_pos):
inc_pos()
return cverts
def str_base(self):
return ", ".join(str(a) for a in self.args)
def __repr__(self):
return "%s" % (self.str_base())
x, y, z, t, u, v = symbols('x,y,z,t,u,v')
default_color_schemes['rainbow'] = ColorScheme(z, y, x)
default_color_schemes['zfade'] = ColorScheme(z, (0.4, 0.4, 0.97),
(0.97, 0.4, 0.4), (None, None, z))
default_color_schemes['zfade3'] = ColorScheme(z, (None, None, z),
[0.00, (0.2, 0.2, 1.0),
0.35, (0.2, 0.8, 0.4),
0.50, (0.3, 0.9, 0.3),
0.65, (0.4, 0.8, 0.2),
1.00, (1.0, 0.2, 0.2)])
default_color_schemes['zfade4'] = ColorScheme(z, (None, None, z),
[0.0, (0.3, 0.3, 1.0),
0.30, (0.3, 1.0, 0.3),
0.55, (0.95, 1.0, 0.2),
0.65, (1.0, 0.95, 0.2),
0.85, (1.0, 0.7, 0.2),
1.0, (1.0, 0.3, 0.2)])

View File

@ -0,0 +1,106 @@
from pyglet.window import Window
from pyglet.clock import Clock
from threading import Thread, Lock
gl_lock = Lock()
class ManagedWindow(Window):
"""
A pyglet window with an event loop which executes automatically
in a separate thread. Behavior is added by creating a subclass
which overrides setup, update, and/or draw.
"""
fps_limit = 30
default_win_args = {"width": 600,
"height": 500,
"vsync": False,
"resizable": True}
def __init__(self, **win_args):
"""
It is best not to override this function in the child
class, unless you need to take additional arguments.
Do any OpenGL initialization calls in setup().
"""
# check if this is run from the doctester
if win_args.get('runfromdoctester', False):
return
self.win_args = dict(self.default_win_args, **win_args)
self.Thread = Thread(target=self.__event_loop__)
self.Thread.start()
def __event_loop__(self, **win_args):
"""
The event loop thread function. Do not override or call
directly (it is called by __init__).
"""
gl_lock.acquire()
try:
try:
super().__init__(**self.win_args)
self.switch_to()
self.setup()
except Exception as e:
print("Window initialization failed: %s" % (str(e)))
self.has_exit = True
finally:
gl_lock.release()
clock = Clock()
clock.fps_limit = self.fps_limit
while not self.has_exit:
dt = clock.tick()
gl_lock.acquire()
try:
try:
self.switch_to()
self.dispatch_events()
self.clear()
self.update(dt)
self.draw()
self.flip()
except Exception as e:
print("Uncaught exception in event loop: %s" % str(e))
self.has_exit = True
finally:
gl_lock.release()
super().close()
def close(self):
"""
Closes the window.
"""
self.has_exit = True
def setup(self):
"""
Called once before the event loop begins.
Override this method in a child class. This
is the best place to put things like OpenGL
initialization calls.
"""
pass
def update(self, dt):
"""
Called before draw during each iteration of
the event loop. dt is the elapsed time in
seconds since the last update. OpenGL rendering
calls are best put in draw() rather than here.
"""
pass
def draw(self):
"""
Called after update during each iteration of
the event loop. Put OpenGL rendering calls
here.
"""
pass
if __name__ == '__main__':
ManagedWindow()

View File

@ -0,0 +1,464 @@
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

View File

@ -0,0 +1,251 @@
import pyglet.gl as pgl
from pyglet import font
from sympy.core import S
from sympy.plotting.pygletplot.plot_object import PlotObject
from sympy.plotting.pygletplot.util import billboard_matrix, dot_product, \
get_direction_vectors, strided_range, vec_mag, vec_sub
from sympy.utilities.iterables import is_sequence
class PlotAxes(PlotObject):
def __init__(self, *args,
style='', none=None, frame=None, box=None, ordinate=None,
stride=0.25,
visible='', overlay='', colored='', label_axes='', label_ticks='',
tick_length=0.1,
font_face='Arial', font_size=28,
**kwargs):
# initialize style parameter
style = style.lower()
# allow alias kwargs to override style kwarg
if none is not None:
style = 'none'
if frame is not None:
style = 'frame'
if box is not None:
style = 'box'
if ordinate is not None:
style = 'ordinate'
if style in ['', 'ordinate']:
self._render_object = PlotAxesOrdinate(self)
elif style in ['frame', 'box']:
self._render_object = PlotAxesFrame(self)
elif style in ['none']:
self._render_object = None
else:
raise ValueError(("Unrecognized axes style %s.") % (style))
# initialize stride parameter
try:
stride = eval(stride)
except TypeError:
pass
if is_sequence(stride):
if len(stride) != 3:
raise ValueError("length should be equal to 3")
self._stride = stride
else:
self._stride = [stride, stride, stride]
self._tick_length = float(tick_length)
# setup bounding box and ticks
self._origin = [0, 0, 0]
self.reset_bounding_box()
def flexible_boolean(input, default):
if input in [True, False]:
return input
if input in ('f', 'F', 'false', 'False'):
return False
if input in ('t', 'T', 'true', 'True'):
return True
return default
# initialize remaining parameters
self.visible = flexible_boolean(kwargs, True)
self._overlay = flexible_boolean(overlay, True)
self._colored = flexible_boolean(colored, False)
self._label_axes = flexible_boolean(label_axes, False)
self._label_ticks = flexible_boolean(label_ticks, True)
# setup label font
self.font_face = font_face
self.font_size = font_size
# this is also used to reinit the
# font on window close/reopen
self.reset_resources()
def reset_resources(self):
self.label_font = None
def reset_bounding_box(self):
self._bounding_box = [[None, None], [None, None], [None, None]]
self._axis_ticks = [[], [], []]
def draw(self):
if self._render_object:
pgl.glPushAttrib(pgl.GL_ENABLE_BIT | pgl.GL_POLYGON_BIT | pgl.GL_DEPTH_BUFFER_BIT)
if self._overlay:
pgl.glDisable(pgl.GL_DEPTH_TEST)
self._render_object.draw()
pgl.glPopAttrib()
def adjust_bounds(self, child_bounds):
b = self._bounding_box
c = child_bounds
for i in range(3):
if abs(c[i][0]) is S.Infinity or abs(c[i][1]) is S.Infinity:
continue
b[i][0] = c[i][0] if b[i][0] is None else min([b[i][0], c[i][0]])
b[i][1] = c[i][1] if b[i][1] is None else max([b[i][1], c[i][1]])
self._bounding_box = b
self._recalculate_axis_ticks(i)
def _recalculate_axis_ticks(self, axis):
b = self._bounding_box
if b[axis][0] is None or b[axis][1] is None:
self._axis_ticks[axis] = []
else:
self._axis_ticks[axis] = strided_range(b[axis][0], b[axis][1],
self._stride[axis])
def toggle_visible(self):
self.visible = not self.visible
def toggle_colors(self):
self._colored = not self._colored
class PlotAxesBase(PlotObject):
def __init__(self, parent_axes):
self._p = parent_axes
def draw(self):
color = [([0.2, 0.1, 0.3], [0.2, 0.1, 0.3], [0.2, 0.1, 0.3]),
([0.9, 0.3, 0.5], [0.5, 1.0, 0.5], [0.3, 0.3, 0.9])][self._p._colored]
self.draw_background(color)
self.draw_axis(2, color[2])
self.draw_axis(1, color[1])
self.draw_axis(0, color[0])
def draw_background(self, color):
pass # optional
def draw_axis(self, axis, color):
raise NotImplementedError()
def draw_text(self, text, position, color, scale=1.0):
if len(color) == 3:
color = (color[0], color[1], color[2], 1.0)
if self._p.label_font is None:
self._p.label_font = font.load(self._p.font_face,
self._p.font_size,
bold=True, italic=False)
label = font.Text(self._p.label_font, text,
color=color,
valign=font.Text.BASELINE,
halign=font.Text.CENTER)
pgl.glPushMatrix()
pgl.glTranslatef(*position)
billboard_matrix()
scale_factor = 0.005 * scale
pgl.glScalef(scale_factor, scale_factor, scale_factor)
pgl.glColor4f(0, 0, 0, 0)
label.draw()
pgl.glPopMatrix()
def draw_line(self, v, color):
o = self._p._origin
pgl.glBegin(pgl.GL_LINES)
pgl.glColor3f(*color)
pgl.glVertex3f(v[0][0] + o[0], v[0][1] + o[1], v[0][2] + o[2])
pgl.glVertex3f(v[1][0] + o[0], v[1][1] + o[1], v[1][2] + o[2])
pgl.glEnd()
class PlotAxesOrdinate(PlotAxesBase):
def __init__(self, parent_axes):
super().__init__(parent_axes)
def draw_axis(self, axis, color):
ticks = self._p._axis_ticks[axis]
radius = self._p._tick_length / 2.0
if len(ticks) < 2:
return
# calculate the vector for this axis
axis_lines = [[0, 0, 0], [0, 0, 0]]
axis_lines[0][axis], axis_lines[1][axis] = ticks[0], ticks[-1]
axis_vector = vec_sub(axis_lines[1], axis_lines[0])
# calculate angle to the z direction vector
pos_z = get_direction_vectors()[2]
d = abs(dot_product(axis_vector, pos_z))
d = d / vec_mag(axis_vector)
# don't draw labels if we're looking down the axis
labels_visible = abs(d - 1.0) > 0.02
# draw the ticks and labels
for tick in ticks:
self.draw_tick_line(axis, color, radius, tick, labels_visible)
# draw the axis line and labels
self.draw_axis_line(axis, color, ticks[0], ticks[-1], labels_visible)
def draw_axis_line(self, axis, color, a_min, a_max, labels_visible):
axis_line = [[0, 0, 0], [0, 0, 0]]
axis_line[0][axis], axis_line[1][axis] = a_min, a_max
self.draw_line(axis_line, color)
if labels_visible:
self.draw_axis_line_labels(axis, color, axis_line)
def draw_axis_line_labels(self, axis, color, axis_line):
if not self._p._label_axes:
return
axis_labels = [axis_line[0][::], axis_line[1][::]]
axis_labels[0][axis] -= 0.3
axis_labels[1][axis] += 0.3
a_str = ['X', 'Y', 'Z'][axis]
self.draw_text("-" + a_str, axis_labels[0], color)
self.draw_text("+" + a_str, axis_labels[1], color)
def draw_tick_line(self, axis, color, radius, tick, labels_visible):
tick_axis = {0: 1, 1: 0, 2: 1}[axis]
tick_line = [[0, 0, 0], [0, 0, 0]]
tick_line[0][axis] = tick_line[1][axis] = tick
tick_line[0][tick_axis], tick_line[1][tick_axis] = -radius, radius
self.draw_line(tick_line, color)
if labels_visible:
self.draw_tick_line_label(axis, color, radius, tick)
def draw_tick_line_label(self, axis, color, radius, tick):
if not self._p._label_axes:
return
tick_label_vector = [0, 0, 0]
tick_label_vector[axis] = tick
tick_label_vector[{0: 1, 1: 0, 2: 1}[axis]] = [-1, 1, 1][
axis] * radius * 3.5
self.draw_text(str(tick), tick_label_vector, color, scale=0.5)
class PlotAxesFrame(PlotAxesBase):
def __init__(self, parent_axes):
super().__init__(parent_axes)
def draw_background(self, color):
pass
def draw_axis(self, axis, color):
raise NotImplementedError()

View File

@ -0,0 +1,124 @@
import pyglet.gl as pgl
from sympy.plotting.pygletplot.plot_rotation import get_spherical_rotatation
from sympy.plotting.pygletplot.util import get_model_matrix, model_to_screen, \
screen_to_model, vec_subs
class PlotCamera:
min_dist = 0.05
max_dist = 500.0
min_ortho_dist = 100.0
max_ortho_dist = 10000.0
_default_dist = 6.0
_default_ortho_dist = 600.0
rot_presets = {
'xy': (0, 0, 0),
'xz': (-90, 0, 0),
'yz': (0, 90, 0),
'perspective': (-45, 0, -45)
}
def __init__(self, window, ortho=False):
self.window = window
self.axes = self.window.plot.axes
self.ortho = ortho
self.reset()
def init_rot_matrix(self):
pgl.glPushMatrix()
pgl.glLoadIdentity()
self._rot = get_model_matrix()
pgl.glPopMatrix()
def set_rot_preset(self, preset_name):
self.init_rot_matrix()
if preset_name not in self.rot_presets:
raise ValueError(
"%s is not a valid rotation preset." % preset_name)
r = self.rot_presets[preset_name]
self.euler_rotate(r[0], 1, 0, 0)
self.euler_rotate(r[1], 0, 1, 0)
self.euler_rotate(r[2], 0, 0, 1)
def reset(self):
self._dist = 0.0
self._x, self._y = 0.0, 0.0
self._rot = None
if self.ortho:
self._dist = self._default_ortho_dist
else:
self._dist = self._default_dist
self.init_rot_matrix()
def mult_rot_matrix(self, rot):
pgl.glPushMatrix()
pgl.glLoadMatrixf(rot)
pgl.glMultMatrixf(self._rot)
self._rot = get_model_matrix()
pgl.glPopMatrix()
def setup_projection(self):
pgl.glMatrixMode(pgl.GL_PROJECTION)
pgl.glLoadIdentity()
if self.ortho:
# yep, this is pseudo ortho (don't tell anyone)
pgl.gluPerspective(
0.3, float(self.window.width)/float(self.window.height),
self.min_ortho_dist - 0.01, self.max_ortho_dist + 0.01)
else:
pgl.gluPerspective(
30.0, float(self.window.width)/float(self.window.height),
self.min_dist - 0.01, self.max_dist + 0.01)
pgl.glMatrixMode(pgl.GL_MODELVIEW)
def _get_scale(self):
return 1.0, 1.0, 1.0
def apply_transformation(self):
pgl.glLoadIdentity()
pgl.glTranslatef(self._x, self._y, -self._dist)
if self._rot is not None:
pgl.glMultMatrixf(self._rot)
pgl.glScalef(*self._get_scale())
def spherical_rotate(self, p1, p2, sensitivity=1.0):
mat = get_spherical_rotatation(p1, p2, self.window.width,
self.window.height, sensitivity)
if mat is not None:
self.mult_rot_matrix(mat)
def euler_rotate(self, angle, x, y, z):
pgl.glPushMatrix()
pgl.glLoadMatrixf(self._rot)
pgl.glRotatef(angle, x, y, z)
self._rot = get_model_matrix()
pgl.glPopMatrix()
def zoom_relative(self, clicks, sensitivity):
if self.ortho:
dist_d = clicks * sensitivity * 50.0
min_dist = self.min_ortho_dist
max_dist = self.max_ortho_dist
else:
dist_d = clicks * sensitivity
min_dist = self.min_dist
max_dist = self.max_dist
new_dist = (self._dist - dist_d)
if (clicks < 0 and new_dist < max_dist) or new_dist > min_dist:
self._dist = new_dist
def mouse_translate(self, x, y, dx, dy):
pgl.glPushMatrix()
pgl.glLoadIdentity()
pgl.glTranslatef(0, 0, -self._dist)
z = model_to_screen(0, 0, 0)[2]
d = vec_subs(screen_to_model(x, y, z), screen_to_model(x - dx, y - dy, z))
pgl.glPopMatrix()
self._x += d[0]
self._y += d[1]

View File

@ -0,0 +1,218 @@
from pyglet.window import key
from pyglet.window.mouse import LEFT, RIGHT, MIDDLE
from sympy.plotting.pygletplot.util import get_direction_vectors, get_basis_vectors
class PlotController:
normal_mouse_sensitivity = 4.0
modified_mouse_sensitivity = 1.0
normal_key_sensitivity = 160.0
modified_key_sensitivity = 40.0
keymap = {
key.LEFT: 'left',
key.A: 'left',
key.NUM_4: 'left',
key.RIGHT: 'right',
key.D: 'right',
key.NUM_6: 'right',
key.UP: 'up',
key.W: 'up',
key.NUM_8: 'up',
key.DOWN: 'down',
key.S: 'down',
key.NUM_2: 'down',
key.Z: 'rotate_z_neg',
key.NUM_1: 'rotate_z_neg',
key.C: 'rotate_z_pos',
key.NUM_3: 'rotate_z_pos',
key.Q: 'spin_left',
key.NUM_7: 'spin_left',
key.E: 'spin_right',
key.NUM_9: 'spin_right',
key.X: 'reset_camera',
key.NUM_5: 'reset_camera',
key.NUM_ADD: 'zoom_in',
key.PAGEUP: 'zoom_in',
key.R: 'zoom_in',
key.NUM_SUBTRACT: 'zoom_out',
key.PAGEDOWN: 'zoom_out',
key.F: 'zoom_out',
key.RSHIFT: 'modify_sensitivity',
key.LSHIFT: 'modify_sensitivity',
key.F1: 'rot_preset_xy',
key.F2: 'rot_preset_xz',
key.F3: 'rot_preset_yz',
key.F4: 'rot_preset_perspective',
key.F5: 'toggle_axes',
key.F6: 'toggle_axe_colors',
key.F8: 'save_image'
}
def __init__(self, window, *, invert_mouse_zoom=False, **kwargs):
self.invert_mouse_zoom = invert_mouse_zoom
self.window = window
self.camera = window.camera
self.action = {
# Rotation around the view Y (up) vector
'left': False,
'right': False,
# Rotation around the view X vector
'up': False,
'down': False,
# Rotation around the view Z vector
'spin_left': False,
'spin_right': False,
# Rotation around the model Z vector
'rotate_z_neg': False,
'rotate_z_pos': False,
# Reset to the default rotation
'reset_camera': False,
# Performs camera z-translation
'zoom_in': False,
'zoom_out': False,
# Use alternative sensitivity (speed)
'modify_sensitivity': False,
# Rotation presets
'rot_preset_xy': False,
'rot_preset_xz': False,
'rot_preset_yz': False,
'rot_preset_perspective': False,
# axes
'toggle_axes': False,
'toggle_axe_colors': False,
# screenshot
'save_image': False
}
def update(self, dt):
z = 0
if self.action['zoom_out']:
z -= 1
if self.action['zoom_in']:
z += 1
if z != 0:
self.camera.zoom_relative(z/10.0, self.get_key_sensitivity()/10.0)
dx, dy, dz = 0, 0, 0
if self.action['left']:
dx -= 1
if self.action['right']:
dx += 1
if self.action['up']:
dy -= 1
if self.action['down']:
dy += 1
if self.action['spin_left']:
dz += 1
if self.action['spin_right']:
dz -= 1
if not self.is_2D():
if dx != 0:
self.camera.euler_rotate(dx*dt*self.get_key_sensitivity(),
*(get_direction_vectors()[1]))
if dy != 0:
self.camera.euler_rotate(dy*dt*self.get_key_sensitivity(),
*(get_direction_vectors()[0]))
if dz != 0:
self.camera.euler_rotate(dz*dt*self.get_key_sensitivity(),
*(get_direction_vectors()[2]))
else:
self.camera.mouse_translate(0, 0, dx*dt*self.get_key_sensitivity(),
-dy*dt*self.get_key_sensitivity())
rz = 0
if self.action['rotate_z_neg'] and not self.is_2D():
rz -= 1
if self.action['rotate_z_pos'] and not self.is_2D():
rz += 1
if rz != 0:
self.camera.euler_rotate(rz*dt*self.get_key_sensitivity(),
*(get_basis_vectors()[2]))
if self.action['reset_camera']:
self.camera.reset()
if self.action['rot_preset_xy']:
self.camera.set_rot_preset('xy')
if self.action['rot_preset_xz']:
self.camera.set_rot_preset('xz')
if self.action['rot_preset_yz']:
self.camera.set_rot_preset('yz')
if self.action['rot_preset_perspective']:
self.camera.set_rot_preset('perspective')
if self.action['toggle_axes']:
self.action['toggle_axes'] = False
self.camera.axes.toggle_visible()
if self.action['toggle_axe_colors']:
self.action['toggle_axe_colors'] = False
self.camera.axes.toggle_colors()
if self.action['save_image']:
self.action['save_image'] = False
self.window.plot.saveimage()
return True
def get_mouse_sensitivity(self):
if self.action['modify_sensitivity']:
return self.modified_mouse_sensitivity
else:
return self.normal_mouse_sensitivity
def get_key_sensitivity(self):
if self.action['modify_sensitivity']:
return self.modified_key_sensitivity
else:
return self.normal_key_sensitivity
def on_key_press(self, symbol, modifiers):
if symbol in self.keymap:
self.action[self.keymap[symbol]] = True
def on_key_release(self, symbol, modifiers):
if symbol in self.keymap:
self.action[self.keymap[symbol]] = False
def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
if buttons & LEFT:
if self.is_2D():
self.camera.mouse_translate(x, y, dx, dy)
else:
self.camera.spherical_rotate((x - dx, y - dy), (x, y),
self.get_mouse_sensitivity())
if buttons & MIDDLE:
self.camera.zoom_relative([1, -1][self.invert_mouse_zoom]*dy,
self.get_mouse_sensitivity()/20.0)
if buttons & RIGHT:
self.camera.mouse_translate(x, y, dx, dy)
def on_mouse_scroll(self, x, y, dx, dy):
self.camera.zoom_relative([1, -1][self.invert_mouse_zoom]*dy,
self.get_mouse_sensitivity())
def is_2D(self):
functions = self.window.plot._functions
for i in functions:
if len(functions[i].i_vars) > 1 or len(functions[i].d_vars) > 2:
return False
return True

View File

@ -0,0 +1,82 @@
import pyglet.gl as pgl
from sympy.core import S
from sympy.plotting.pygletplot.plot_mode_base import PlotModeBase
class PlotCurve(PlotModeBase):
style_override = 'wireframe'
def _on_calculate_verts(self):
self.t_interval = self.intervals[0]
self.t_set = list(self.t_interval.frange())
self.bounds = [[S.Infinity, S.NegativeInfinity, 0],
[S.Infinity, S.NegativeInfinity, 0],
[S.Infinity, S.NegativeInfinity, 0]]
evaluate = self._get_evaluator()
self._calculating_verts_pos = 0.0
self._calculating_verts_len = float(self.t_interval.v_len)
self.verts = []
b = self.bounds
for t in self.t_set:
try:
_e = evaluate(t) # calculate vertex
except (NameError, ZeroDivisionError):
_e = None
if _e is not None: # update bounding box
for axis in range(3):
b[axis][0] = min([b[axis][0], _e[axis]])
b[axis][1] = max([b[axis][1], _e[axis]])
self.verts.append(_e)
self._calculating_verts_pos += 1.0
for axis in range(3):
b[axis][2] = b[axis][1] - b[axis][0]
if b[axis][2] == 0.0:
b[axis][2] = 1.0
self.push_wireframe(self.draw_verts(False))
def _on_calculate_cverts(self):
if not self.verts or not self.color:
return
def set_work_len(n):
self._calculating_cverts_len = float(n)
def inc_work_pos():
self._calculating_cverts_pos += 1.0
set_work_len(1)
self._calculating_cverts_pos = 0
self.cverts = self.color.apply_to_curve(self.verts,
self.t_set,
set_len=set_work_len,
inc_pos=inc_work_pos)
self.push_wireframe(self.draw_verts(True))
def calculate_one_cvert(self, t):
vert = self.verts[t]
return self.color(vert[0], vert[1], vert[2],
self.t_set[t], None)
def draw_verts(self, use_cverts):
def f():
pgl.glBegin(pgl.GL_LINE_STRIP)
for t in range(len(self.t_set)):
p = self.verts[t]
if p is None:
pgl.glEnd()
pgl.glBegin(pgl.GL_LINE_STRIP)
continue
if use_cverts:
c = self.cverts[t]
if c is None:
c = (0, 0, 0)
pgl.glColor3f(*c)
else:
pgl.glColor3f(*self.default_wireframe_color)
pgl.glVertex3f(*p)
pgl.glEnd()
return f

View File

@ -0,0 +1,181 @@
from sympy.core.singleton import S
from sympy.core.symbol import Symbol
from sympy.core.sympify import sympify
from sympy.core.numbers import Integer
class PlotInterval:
"""
"""
_v, _v_min, _v_max, _v_steps = None, None, None, None
def require_all_args(f):
def check(self, *args, **kwargs):
for g in [self._v, self._v_min, self._v_max, self._v_steps]:
if g is None:
raise ValueError("PlotInterval is incomplete.")
return f(self, *args, **kwargs)
return check
def __init__(self, *args):
if len(args) == 1:
if isinstance(args[0], PlotInterval):
self.fill_from(args[0])
return
elif isinstance(args[0], str):
try:
args = eval(args[0])
except TypeError:
s_eval_error = "Could not interpret string %s."
raise ValueError(s_eval_error % (args[0]))
elif isinstance(args[0], (tuple, list)):
args = args[0]
else:
raise ValueError("Not an interval.")
if not isinstance(args, (tuple, list)) or len(args) > 4:
f_error = "PlotInterval must be a tuple or list of length 4 or less."
raise ValueError(f_error)
args = list(args)
if len(args) > 0 and (args[0] is None or isinstance(args[0], Symbol)):
self.v = args.pop(0)
if len(args) in [2, 3]:
self.v_min = args.pop(0)
self.v_max = args.pop(0)
if len(args) == 1:
self.v_steps = args.pop(0)
elif len(args) == 1:
self.v_steps = args.pop(0)
def get_v(self):
return self._v
def set_v(self, v):
if v is None:
self._v = None
return
if not isinstance(v, Symbol):
raise ValueError("v must be a SymPy Symbol.")
self._v = v
def get_v_min(self):
return self._v_min
def set_v_min(self, v_min):
if v_min is None:
self._v_min = None
return
try:
self._v_min = sympify(v_min)
float(self._v_min.evalf())
except TypeError:
raise ValueError("v_min could not be interpreted as a number.")
def get_v_max(self):
return self._v_max
def set_v_max(self, v_max):
if v_max is None:
self._v_max = None
return
try:
self._v_max = sympify(v_max)
float(self._v_max.evalf())
except TypeError:
raise ValueError("v_max could not be interpreted as a number.")
def get_v_steps(self):
return self._v_steps
def set_v_steps(self, v_steps):
if v_steps is None:
self._v_steps = None
return
if isinstance(v_steps, int):
v_steps = Integer(v_steps)
elif not isinstance(v_steps, Integer):
raise ValueError("v_steps must be an int or SymPy Integer.")
if v_steps <= S.Zero:
raise ValueError("v_steps must be positive.")
self._v_steps = v_steps
@require_all_args
def get_v_len(self):
return self.v_steps + 1
v = property(get_v, set_v)
v_min = property(get_v_min, set_v_min)
v_max = property(get_v_max, set_v_max)
v_steps = property(get_v_steps, set_v_steps)
v_len = property(get_v_len)
def fill_from(self, b):
if b.v is not None:
self.v = b.v
if b.v_min is not None:
self.v_min = b.v_min
if b.v_max is not None:
self.v_max = b.v_max
if b.v_steps is not None:
self.v_steps = b.v_steps
@staticmethod
def try_parse(*args):
"""
Returns a PlotInterval if args can be interpreted
as such, otherwise None.
"""
if len(args) == 1 and isinstance(args[0], PlotInterval):
return args[0]
try:
return PlotInterval(*args)
except ValueError:
return None
def _str_base(self):
return ",".join([str(self.v), str(self.v_min),
str(self.v_max), str(self.v_steps)])
def __repr__(self):
"""
A string representing the interval in class constructor form.
"""
return "PlotInterval(%s)" % (self._str_base())
def __str__(self):
"""
A string representing the interval in list form.
"""
return "[%s]" % (self._str_base())
@require_all_args
def assert_complete(self):
pass
@require_all_args
def vrange(self):
"""
Yields v_steps+1 SymPy numbers ranging from
v_min to v_max.
"""
d = (self.v_max - self.v_min) / self.v_steps
for i in range(self.v_steps + 1):
a = self.v_min + (d * Integer(i))
yield a
@require_all_args
def vrange2(self):
"""
Yields v_steps pairs of SymPy numbers ranging from
(v_min, v_min + step) to (v_max - step, v_max).
"""
d = (self.v_max - self.v_min) / self.v_steps
a = self.v_min + (d * S.Zero)
for i in range(self.v_steps):
b = self.v_min + (d * Integer(i + 1))
yield a, b
a = b
def frange(self):
for i in self.vrange():
yield float(i.evalf())

View File

@ -0,0 +1,400 @@
from .plot_interval import PlotInterval
from .plot_object import PlotObject
from .util import parse_option_string
from sympy.core.symbol import Symbol
from sympy.core.sympify import sympify
from sympy.geometry.entity import GeometryEntity
from sympy.utilities.iterables import is_sequence
class PlotMode(PlotObject):
"""
Grandparent class for plotting
modes. Serves as interface for
registration, lookup, and init
of modes.
To create a new plot mode,
inherit from PlotModeBase
or one of its children, such
as PlotSurface or PlotCurve.
"""
## Class-level attributes
## used to register and lookup
## plot modes. See PlotModeBase
## for descriptions and usage.
i_vars, d_vars = '', ''
intervals = []
aliases = []
is_default = False
## Draw is the only method here which
## is meant to be overridden in child
## classes, and PlotModeBase provides
## a base implementation.
def draw(self):
raise NotImplementedError()
## Everything else in this file has to
## do with registration and retrieval
## of plot modes. This is where I've
## hidden much of the ugliness of automatic
## plot mode divination...
## Plot mode registry data structures
_mode_alias_list = []
_mode_map = {
1: {1: {}, 2: {}},
2: {1: {}, 2: {}},
3: {1: {}, 2: {}},
} # [d][i][alias_str]: class
_mode_default_map = {
1: {},
2: {},
3: {},
} # [d][i]: class
_i_var_max, _d_var_max = 2, 3
def __new__(cls, *args, **kwargs):
"""
This is the function which interprets
arguments given to Plot.__init__ and
Plot.__setattr__. Returns an initialized
instance of the appropriate child class.
"""
newargs, newkwargs = PlotMode._extract_options(args, kwargs)
mode_arg = newkwargs.get('mode', '')
# Interpret the arguments
d_vars, intervals = PlotMode._interpret_args(newargs)
i_vars = PlotMode._find_i_vars(d_vars, intervals)
i, d = max([len(i_vars), len(intervals)]), len(d_vars)
# Find the appropriate mode
subcls = PlotMode._get_mode(mode_arg, i, d)
# Create the object
o = object.__new__(subcls)
# Do some setup for the mode instance
o.d_vars = d_vars
o._fill_i_vars(i_vars)
o._fill_intervals(intervals)
o.options = newkwargs
return o
@staticmethod
def _get_mode(mode_arg, i_var_count, d_var_count):
"""
Tries to return an appropriate mode class.
Intended to be called only by __new__.
mode_arg
Can be a string or a class. If it is a
PlotMode subclass, it is simply returned.
If it is a string, it can an alias for
a mode or an empty string. In the latter
case, we try to find a default mode for
the i_var_count and d_var_count.
i_var_count
The number of independent variables
needed to evaluate the d_vars.
d_var_count
The number of dependent variables;
usually the number of functions to
be evaluated in plotting.
For example, a Cartesian function y = f(x) has
one i_var (x) and one d_var (y). A parametric
form x,y,z = f(u,v), f(u,v), f(u,v) has two
two i_vars (u,v) and three d_vars (x,y,z).
"""
# if the mode_arg is simply a PlotMode class,
# check that the mode supports the numbers
# of independent and dependent vars, then
# return it
try:
m = None
if issubclass(mode_arg, PlotMode):
m = mode_arg
except TypeError:
pass
if m:
if not m._was_initialized:
raise ValueError(("To use unregistered plot mode %s "
"you must first call %s._init_mode().")
% (m.__name__, m.__name__))
if d_var_count != m.d_var_count:
raise ValueError(("%s can only plot functions "
"with %i dependent variables.")
% (m.__name__,
m.d_var_count))
if i_var_count > m.i_var_count:
raise ValueError(("%s cannot plot functions "
"with more than %i independent "
"variables.")
% (m.__name__,
m.i_var_count))
return m
# If it is a string, there are two possibilities.
if isinstance(mode_arg, str):
i, d = i_var_count, d_var_count
if i > PlotMode._i_var_max:
raise ValueError(var_count_error(True, True))
if d > PlotMode._d_var_max:
raise ValueError(var_count_error(False, True))
# If the string is '', try to find a suitable
# default mode
if not mode_arg:
return PlotMode._get_default_mode(i, d)
# Otherwise, interpret the string as a mode
# alias (e.g. 'cartesian', 'parametric', etc)
else:
return PlotMode._get_aliased_mode(mode_arg, i, d)
else:
raise ValueError("PlotMode argument must be "
"a class or a string")
@staticmethod
def _get_default_mode(i, d, i_vars=-1):
if i_vars == -1:
i_vars = i
try:
return PlotMode._mode_default_map[d][i]
except KeyError:
# Keep looking for modes in higher i var counts
# which support the given d var count until we
# reach the max i_var count.
if i < PlotMode._i_var_max:
return PlotMode._get_default_mode(i + 1, d, i_vars)
else:
raise ValueError(("Couldn't find a default mode "
"for %i independent and %i "
"dependent variables.") % (i_vars, d))
@staticmethod
def _get_aliased_mode(alias, i, d, i_vars=-1):
if i_vars == -1:
i_vars = i
if alias not in PlotMode._mode_alias_list:
raise ValueError(("Couldn't find a mode called"
" %s. Known modes: %s.")
% (alias, ", ".join(PlotMode._mode_alias_list)))
try:
return PlotMode._mode_map[d][i][alias]
except TypeError:
# Keep looking for modes in higher i var counts
# which support the given d var count and alias
# until we reach the max i_var count.
if i < PlotMode._i_var_max:
return PlotMode._get_aliased_mode(alias, i + 1, d, i_vars)
else:
raise ValueError(("Couldn't find a %s mode "
"for %i independent and %i "
"dependent variables.")
% (alias, i_vars, d))
@classmethod
def _register(cls):
"""
Called once for each user-usable plot mode.
For Cartesian2D, it is invoked after the
class definition: Cartesian2D._register()
"""
name = cls.__name__
cls._init_mode()
try:
i, d = cls.i_var_count, cls.d_var_count
# Add the mode to _mode_map under all
# given aliases
for a in cls.aliases:
if a not in PlotMode._mode_alias_list:
# Also track valid aliases, so
# we can quickly know when given
# an invalid one in _get_mode.
PlotMode._mode_alias_list.append(a)
PlotMode._mode_map[d][i][a] = cls
if cls.is_default:
# If this mode was marked as the
# default for this d,i combination,
# also set that.
PlotMode._mode_default_map[d][i] = cls
except Exception as e:
raise RuntimeError(("Failed to register "
"plot mode %s. Reason: %s")
% (name, (str(e))))
@classmethod
def _init_mode(cls):
"""
Initializes the plot mode based on
the 'mode-specific parameters' above.
Only intended to be called by
PlotMode._register(). To use a mode without
registering it, you can directly call
ModeSubclass._init_mode().
"""
def symbols_list(symbol_str):
return [Symbol(s) for s in symbol_str]
# Convert the vars strs into
# lists of symbols.
cls.i_vars = symbols_list(cls.i_vars)
cls.d_vars = symbols_list(cls.d_vars)
# Var count is used often, calculate
# it once here
cls.i_var_count = len(cls.i_vars)
cls.d_var_count = len(cls.d_vars)
if cls.i_var_count > PlotMode._i_var_max:
raise ValueError(var_count_error(True, False))
if cls.d_var_count > PlotMode._d_var_max:
raise ValueError(var_count_error(False, False))
# Try to use first alias as primary_alias
if len(cls.aliases) > 0:
cls.primary_alias = cls.aliases[0]
else:
cls.primary_alias = cls.__name__
di = cls.intervals
if len(di) != cls.i_var_count:
raise ValueError("Plot mode must provide a "
"default interval for each i_var.")
for i in range(cls.i_var_count):
# default intervals must be given [min,max,steps]
# (no var, but they must be in the same order as i_vars)
if len(di[i]) != 3:
raise ValueError("length should be equal to 3")
# Initialize an incomplete interval,
# to later be filled with a var when
# the mode is instantiated.
di[i] = PlotInterval(None, *di[i])
# To prevent people from using modes
# without these required fields set up.
cls._was_initialized = True
_was_initialized = False
## Initializer Helper Methods
@staticmethod
def _find_i_vars(functions, intervals):
i_vars = []
# First, collect i_vars in the
# order they are given in any
# intervals.
for i in intervals:
if i.v is None:
continue
elif i.v in i_vars:
raise ValueError(("Multiple intervals given "
"for %s.") % (str(i.v)))
i_vars.append(i.v)
# Then, find any remaining
# i_vars in given functions
# (aka d_vars)
for f in functions:
for a in f.free_symbols:
if a not in i_vars:
i_vars.append(a)
return i_vars
def _fill_i_vars(self, i_vars):
# copy default i_vars
self.i_vars = [Symbol(str(i)) for i in self.i_vars]
# replace with given i_vars
for i in range(len(i_vars)):
self.i_vars[i] = i_vars[i]
def _fill_intervals(self, intervals):
# copy default intervals
self.intervals = [PlotInterval(i) for i in self.intervals]
# track i_vars used so far
v_used = []
# fill copy of default
# intervals with given info
for i in range(len(intervals)):
self.intervals[i].fill_from(intervals[i])
if self.intervals[i].v is not None:
v_used.append(self.intervals[i].v)
# Find any orphan intervals and
# assign them i_vars
for i in range(len(self.intervals)):
if self.intervals[i].v is None:
u = [v for v in self.i_vars if v not in v_used]
if len(u) == 0:
raise ValueError("length should not be equal to 0")
self.intervals[i].v = u[0]
v_used.append(u[0])
@staticmethod
def _interpret_args(args):
interval_wrong_order = "PlotInterval %s was given before any function(s)."
interpret_error = "Could not interpret %s as a function or interval."
functions, intervals = [], []
if isinstance(args[0], GeometryEntity):
for coords in list(args[0].arbitrary_point()):
functions.append(coords)
intervals.append(PlotInterval.try_parse(args[0].plot_interval()))
else:
for a in args:
i = PlotInterval.try_parse(a)
if i is not None:
if len(functions) == 0:
raise ValueError(interval_wrong_order % (str(i)))
else:
intervals.append(i)
else:
if is_sequence(a, include=str):
raise ValueError(interpret_error % (str(a)))
try:
f = sympify(a)
functions.append(f)
except TypeError:
raise ValueError(interpret_error % str(a))
return functions, intervals
@staticmethod
def _extract_options(args, kwargs):
newkwargs, newargs = {}, []
for a in args:
if isinstance(a, str):
newkwargs = dict(newkwargs, **parse_option_string(a))
else:
newargs.append(a)
newkwargs = dict(newkwargs, **kwargs)
return newargs, newkwargs
def var_count_error(is_independent, is_plotting):
"""
Used to format an error message which differs
slightly in 4 places.
"""
if is_plotting:
v = "Plotting"
else:
v = "Registering plot modes"
if is_independent:
n, s = PlotMode._i_var_max, "independent"
else:
n, s = PlotMode._d_var_max, "dependent"
return ("%s with more than %i %s variables "
"is not supported.") % (v, n, s)

View File

@ -0,0 +1,378 @@
import pyglet.gl as pgl
from sympy.core import S
from sympy.plotting.pygletplot.color_scheme import ColorScheme
from sympy.plotting.pygletplot.plot_mode import PlotMode
from sympy.utilities.iterables import is_sequence
from time import sleep
from threading import Thread, Event, RLock
import warnings
class PlotModeBase(PlotMode):
"""
Intended parent class for plotting
modes. Provides base functionality
in conjunction with its parent,
PlotMode.
"""
##
## Class-Level Attributes
##
"""
The following attributes are meant
to be set at the class level, and serve
as parameters to the plot mode registry
(in PlotMode). See plot_modes.py for
concrete examples.
"""
"""
i_vars
'x' for Cartesian2D
'xy' for Cartesian3D
etc.
d_vars
'y' for Cartesian2D
'r' for Polar
etc.
"""
i_vars, d_vars = '', ''
"""
intervals
Default intervals for each i_var, and in the
same order. Specified [min, max, steps].
No variable can be given (it is bound later).
"""
intervals = []
"""
aliases
A list of strings which can be used to
access this mode.
'cartesian' for Cartesian2D and Cartesian3D
'polar' for Polar
'cylindrical', 'polar' for Cylindrical
Note that _init_mode chooses the first alias
in the list as the mode's primary_alias, which
will be displayed to the end user in certain
contexts.
"""
aliases = []
"""
is_default
Whether to set this mode as the default
for arguments passed to PlotMode() containing
the same number of d_vars as this mode and
at most the same number of i_vars.
"""
is_default = False
"""
All of the above attributes are defined in PlotMode.
The following ones are specific to PlotModeBase.
"""
"""
A list of the render styles. Do not modify.
"""
styles = {'wireframe': 1, 'solid': 2, 'both': 3}
"""
style_override
Always use this style if not blank.
"""
style_override = ''
"""
default_wireframe_color
default_solid_color
Can be used when color is None or being calculated.
Used by PlotCurve and PlotSurface, but not anywhere
in PlotModeBase.
"""
default_wireframe_color = (0.85, 0.85, 0.85)
default_solid_color = (0.6, 0.6, 0.9)
default_rot_preset = 'xy'
##
## Instance-Level Attributes
##
## 'Abstract' member functions
def _get_evaluator(self):
if self.use_lambda_eval:
try:
e = self._get_lambda_evaluator()
return e
except Exception:
warnings.warn("\nWarning: creating lambda evaluator failed. "
"Falling back on SymPy subs evaluator.")
return self._get_sympy_evaluator()
def _get_sympy_evaluator(self):
raise NotImplementedError()
def _get_lambda_evaluator(self):
raise NotImplementedError()
def _on_calculate_verts(self):
raise NotImplementedError()
def _on_calculate_cverts(self):
raise NotImplementedError()
## Base member functions
def __init__(self, *args, bounds_callback=None, **kwargs):
self.verts = []
self.cverts = []
self.bounds = [[S.Infinity, S.NegativeInfinity, 0],
[S.Infinity, S.NegativeInfinity, 0],
[S.Infinity, S.NegativeInfinity, 0]]
self.cbounds = [[S.Infinity, S.NegativeInfinity, 0],
[S.Infinity, S.NegativeInfinity, 0],
[S.Infinity, S.NegativeInfinity, 0]]
self._draw_lock = RLock()
self._calculating_verts = Event()
self._calculating_cverts = Event()
self._calculating_verts_pos = 0.0
self._calculating_verts_len = 0.0
self._calculating_cverts_pos = 0.0
self._calculating_cverts_len = 0.0
self._max_render_stack_size = 3
self._draw_wireframe = [-1]
self._draw_solid = [-1]
self._style = None
self._color = None
self.predraw = []
self.postdraw = []
self.use_lambda_eval = self.options.pop('use_sympy_eval', None) is None
self.style = self.options.pop('style', '')
self.color = self.options.pop('color', 'rainbow')
self.bounds_callback = bounds_callback
self._on_calculate()
def synchronized(f):
def w(self, *args, **kwargs):
self._draw_lock.acquire()
try:
r = f(self, *args, **kwargs)
return r
finally:
self._draw_lock.release()
return w
@synchronized
def push_wireframe(self, function):
"""
Push a function which performs gl commands
used to build a display list. (The list is
built outside of the function)
"""
assert callable(function)
self._draw_wireframe.append(function)
if len(self._draw_wireframe) > self._max_render_stack_size:
del self._draw_wireframe[1] # leave marker element
@synchronized
def push_solid(self, function):
"""
Push a function which performs gl commands
used to build a display list. (The list is
built outside of the function)
"""
assert callable(function)
self._draw_solid.append(function)
if len(self._draw_solid) > self._max_render_stack_size:
del self._draw_solid[1] # leave marker element
def _create_display_list(self, function):
dl = pgl.glGenLists(1)
pgl.glNewList(dl, pgl.GL_COMPILE)
function()
pgl.glEndList()
return dl
def _render_stack_top(self, render_stack):
top = render_stack[-1]
if top == -1:
return -1 # nothing to display
elif callable(top):
dl = self._create_display_list(top)
render_stack[-1] = (dl, top)
return dl # display newly added list
elif len(top) == 2:
if pgl.GL_TRUE == pgl.glIsList(top[0]):
return top[0] # display stored list
dl = self._create_display_list(top[1])
render_stack[-1] = (dl, top[1])
return dl # display regenerated list
def _draw_solid_display_list(self, dl):
pgl.glPushAttrib(pgl.GL_ENABLE_BIT | pgl.GL_POLYGON_BIT)
pgl.glPolygonMode(pgl.GL_FRONT_AND_BACK, pgl.GL_FILL)
pgl.glCallList(dl)
pgl.glPopAttrib()
def _draw_wireframe_display_list(self, dl):
pgl.glPushAttrib(pgl.GL_ENABLE_BIT | pgl.GL_POLYGON_BIT)
pgl.glPolygonMode(pgl.GL_FRONT_AND_BACK, pgl.GL_LINE)
pgl.glEnable(pgl.GL_POLYGON_OFFSET_LINE)
pgl.glPolygonOffset(-0.005, -50.0)
pgl.glCallList(dl)
pgl.glPopAttrib()
@synchronized
def draw(self):
for f in self.predraw:
if callable(f):
f()
if self.style_override:
style = self.styles[self.style_override]
else:
style = self.styles[self._style]
# Draw solid component if style includes solid
if style & 2:
dl = self._render_stack_top(self._draw_solid)
if dl > 0 and pgl.GL_TRUE == pgl.glIsList(dl):
self._draw_solid_display_list(dl)
# Draw wireframe component if style includes wireframe
if style & 1:
dl = self._render_stack_top(self._draw_wireframe)
if dl > 0 and pgl.GL_TRUE == pgl.glIsList(dl):
self._draw_wireframe_display_list(dl)
for f in self.postdraw:
if callable(f):
f()
def _on_change_color(self, color):
Thread(target=self._calculate_cverts).start()
def _on_calculate(self):
Thread(target=self._calculate_all).start()
def _calculate_all(self):
self._calculate_verts()
self._calculate_cverts()
def _calculate_verts(self):
if self._calculating_verts.is_set():
return
self._calculating_verts.set()
try:
self._on_calculate_verts()
finally:
self._calculating_verts.clear()
if callable(self.bounds_callback):
self.bounds_callback()
def _calculate_cverts(self):
if self._calculating_verts.is_set():
return
while self._calculating_cverts.is_set():
sleep(0) # wait for previous calculation
self._calculating_cverts.set()
try:
self._on_calculate_cverts()
finally:
self._calculating_cverts.clear()
def _get_calculating_verts(self):
return self._calculating_verts.is_set()
def _get_calculating_verts_pos(self):
return self._calculating_verts_pos
def _get_calculating_verts_len(self):
return self._calculating_verts_len
def _get_calculating_cverts(self):
return self._calculating_cverts.is_set()
def _get_calculating_cverts_pos(self):
return self._calculating_cverts_pos
def _get_calculating_cverts_len(self):
return self._calculating_cverts_len
## Property handlers
def _get_style(self):
return self._style
@synchronized
def _set_style(self, v):
if v is None:
return
if v == '':
step_max = 0
for i in self.intervals:
if i.v_steps is None:
continue
step_max = max([step_max, int(i.v_steps)])
v = ['both', 'solid'][step_max > 40]
if v not in self.styles:
raise ValueError("v should be there in self.styles")
if v == self._style:
return
self._style = v
def _get_color(self):
return self._color
@synchronized
def _set_color(self, v):
try:
if v is not None:
if is_sequence(v):
v = ColorScheme(*v)
else:
v = ColorScheme(v)
if repr(v) == repr(self._color):
return
self._on_change_color(v)
self._color = v
except Exception as e:
raise RuntimeError("Color change failed. "
"Reason: %s" % (str(e)))
style = property(_get_style, _set_style)
color = property(_get_color, _set_color)
calculating_verts = property(_get_calculating_verts)
calculating_verts_pos = property(_get_calculating_verts_pos)
calculating_verts_len = property(_get_calculating_verts_len)
calculating_cverts = property(_get_calculating_cverts)
calculating_cverts_pos = property(_get_calculating_cverts_pos)
calculating_cverts_len = property(_get_calculating_cverts_len)
## String representations
def __str__(self):
f = ", ".join(str(d) for d in self.d_vars)
o = "'mode=%s'" % (self.primary_alias)
return ", ".join([f, o])
def __repr__(self):
f = ", ".join(str(d) for d in self.d_vars)
i = ", ".join(str(i) for i in self.intervals)
d = [('mode', self.primary_alias),
('color', str(self.color)),
('style', str(self.style))]
o = "'%s'" % ("; ".join("%s=%s" % (k, v)
for k, v in d if v != 'None'))
return ", ".join([f, i, o])

View File

@ -0,0 +1,209 @@
from sympy.utilities.lambdify import lambdify
from sympy.core.numbers import pi
from sympy.functions import sin, cos
from sympy.plotting.pygletplot.plot_curve import PlotCurve
from sympy.plotting.pygletplot.plot_surface import PlotSurface
from math import sin as p_sin
from math import cos as p_cos
def float_vec3(f):
def inner(*args):
v = f(*args)
return float(v[0]), float(v[1]), float(v[2])
return inner
class Cartesian2D(PlotCurve):
i_vars, d_vars = 'x', 'y'
intervals = [[-5, 5, 100]]
aliases = ['cartesian']
is_default = True
def _get_sympy_evaluator(self):
fy = self.d_vars[0]
x = self.t_interval.v
@float_vec3
def e(_x):
return (_x, fy.subs(x, _x), 0.0)
return e
def _get_lambda_evaluator(self):
fy = self.d_vars[0]
x = self.t_interval.v
return lambdify([x], [x, fy, 0.0])
class Cartesian3D(PlotSurface):
i_vars, d_vars = 'xy', 'z'
intervals = [[-1, 1, 40], [-1, 1, 40]]
aliases = ['cartesian', 'monge']
is_default = True
def _get_sympy_evaluator(self):
fz = self.d_vars[0]
x = self.u_interval.v
y = self.v_interval.v
@float_vec3
def e(_x, _y):
return (_x, _y, fz.subs(x, _x).subs(y, _y))
return e
def _get_lambda_evaluator(self):
fz = self.d_vars[0]
x = self.u_interval.v
y = self.v_interval.v
return lambdify([x, y], [x, y, fz])
class ParametricCurve2D(PlotCurve):
i_vars, d_vars = 't', 'xy'
intervals = [[0, 2*pi, 100]]
aliases = ['parametric']
is_default = True
def _get_sympy_evaluator(self):
fx, fy = self.d_vars
t = self.t_interval.v
@float_vec3
def e(_t):
return (fx.subs(t, _t), fy.subs(t, _t), 0.0)
return e
def _get_lambda_evaluator(self):
fx, fy = self.d_vars
t = self.t_interval.v
return lambdify([t], [fx, fy, 0.0])
class ParametricCurve3D(PlotCurve):
i_vars, d_vars = 't', 'xyz'
intervals = [[0, 2*pi, 100]]
aliases = ['parametric']
is_default = True
def _get_sympy_evaluator(self):
fx, fy, fz = self.d_vars
t = self.t_interval.v
@float_vec3
def e(_t):
return (fx.subs(t, _t), fy.subs(t, _t), fz.subs(t, _t))
return e
def _get_lambda_evaluator(self):
fx, fy, fz = self.d_vars
t = self.t_interval.v
return lambdify([t], [fx, fy, fz])
class ParametricSurface(PlotSurface):
i_vars, d_vars = 'uv', 'xyz'
intervals = [[-1, 1, 40], [-1, 1, 40]]
aliases = ['parametric']
is_default = True
def _get_sympy_evaluator(self):
fx, fy, fz = self.d_vars
u = self.u_interval.v
v = self.v_interval.v
@float_vec3
def e(_u, _v):
return (fx.subs(u, _u).subs(v, _v),
fy.subs(u, _u).subs(v, _v),
fz.subs(u, _u).subs(v, _v))
return e
def _get_lambda_evaluator(self):
fx, fy, fz = self.d_vars
u = self.u_interval.v
v = self.v_interval.v
return lambdify([u, v], [fx, fy, fz])
class Polar(PlotCurve):
i_vars, d_vars = 't', 'r'
intervals = [[0, 2*pi, 100]]
aliases = ['polar']
is_default = False
def _get_sympy_evaluator(self):
fr = self.d_vars[0]
t = self.t_interval.v
def e(_t):
_r = float(fr.subs(t, _t))
return (_r*p_cos(_t), _r*p_sin(_t), 0.0)
return e
def _get_lambda_evaluator(self):
fr = self.d_vars[0]
t = self.t_interval.v
fx, fy = fr*cos(t), fr*sin(t)
return lambdify([t], [fx, fy, 0.0])
class Cylindrical(PlotSurface):
i_vars, d_vars = 'th', 'r'
intervals = [[0, 2*pi, 40], [-1, 1, 20]]
aliases = ['cylindrical', 'polar']
is_default = False
def _get_sympy_evaluator(self):
fr = self.d_vars[0]
t = self.u_interval.v
h = self.v_interval.v
def e(_t, _h):
_r = float(fr.subs(t, _t).subs(h, _h))
return (_r*p_cos(_t), _r*p_sin(_t), _h)
return e
def _get_lambda_evaluator(self):
fr = self.d_vars[0]
t = self.u_interval.v
h = self.v_interval.v
fx, fy = fr*cos(t), fr*sin(t)
return lambdify([t, h], [fx, fy, h])
class Spherical(PlotSurface):
i_vars, d_vars = 'tp', 'r'
intervals = [[0, 2*pi, 40], [0, pi, 20]]
aliases = ['spherical']
is_default = False
def _get_sympy_evaluator(self):
fr = self.d_vars[0]
t = self.u_interval.v
p = self.v_interval.v
def e(_t, _p):
_r = float(fr.subs(t, _t).subs(p, _p))
return (_r*p_cos(_t)*p_sin(_p),
_r*p_sin(_t)*p_sin(_p),
_r*p_cos(_p))
return e
def _get_lambda_evaluator(self):
fr = self.d_vars[0]
t = self.u_interval.v
p = self.v_interval.v
fx = fr * cos(t) * sin(p)
fy = fr * sin(t) * sin(p)
fz = fr * cos(p)
return lambdify([t, p], [fx, fy, fz])
Cartesian2D._register()
Cartesian3D._register()
ParametricCurve2D._register()
ParametricCurve3D._register()
ParametricSurface._register()
Polar._register()
Cylindrical._register()
Spherical._register()

View File

@ -0,0 +1,17 @@
class PlotObject:
"""
Base class for objects which can be displayed in
a Plot.
"""
visible = True
def _draw(self):
if self.visible:
self.draw()
def draw(self):
"""
OpenGL rendering code for the plot object.
Override in base class.
"""
pass

View File

@ -0,0 +1,68 @@
try:
from ctypes import c_float
except ImportError:
pass
import pyglet.gl as pgl
from math import sqrt as _sqrt, acos as _acos
def cross(a, b):
return (a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0])
def dot(a, b):
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
def mag(a):
return _sqrt(a[0]**2 + a[1]**2 + a[2]**2)
def norm(a):
m = mag(a)
return (a[0] / m, a[1] / m, a[2] / m)
def get_sphere_mapping(x, y, width, height):
x = min([max([x, 0]), width])
y = min([max([y, 0]), height])
sr = _sqrt((width/2)**2 + (height/2)**2)
sx = ((x - width / 2) / sr)
sy = ((y - height / 2) / sr)
sz = 1.0 - sx**2 - sy**2
if sz > 0.0:
sz = _sqrt(sz)
return (sx, sy, sz)
else:
sz = 0
return norm((sx, sy, sz))
rad2deg = 180.0 / 3.141592
def get_spherical_rotatation(p1, p2, width, height, theta_multiplier):
v1 = get_sphere_mapping(p1[0], p1[1], width, height)
v2 = get_sphere_mapping(p2[0], p2[1], width, height)
d = min(max([dot(v1, v2), -1]), 1)
if abs(d - 1.0) < 0.000001:
return None
raxis = norm( cross(v1, v2) )
rtheta = theta_multiplier * rad2deg * _acos(d)
pgl.glPushMatrix()
pgl.glLoadIdentity()
pgl.glRotatef(rtheta, *raxis)
mat = (c_float*16)()
pgl.glGetFloatv(pgl.GL_MODELVIEW_MATRIX, mat)
pgl.glPopMatrix()
return mat

View File

@ -0,0 +1,102 @@
import pyglet.gl as pgl
from sympy.core import S
from sympy.plotting.pygletplot.plot_mode_base import PlotModeBase
class PlotSurface(PlotModeBase):
default_rot_preset = 'perspective'
def _on_calculate_verts(self):
self.u_interval = self.intervals[0]
self.u_set = list(self.u_interval.frange())
self.v_interval = self.intervals[1]
self.v_set = list(self.v_interval.frange())
self.bounds = [[S.Infinity, S.NegativeInfinity, 0],
[S.Infinity, S.NegativeInfinity, 0],
[S.Infinity, S.NegativeInfinity, 0]]
evaluate = self._get_evaluator()
self._calculating_verts_pos = 0.0
self._calculating_verts_len = float(
self.u_interval.v_len*self.v_interval.v_len)
verts = []
b = self.bounds
for u in self.u_set:
column = []
for v in self.v_set:
try:
_e = evaluate(u, v) # calculate vertex
except ZeroDivisionError:
_e = None
if _e is not None: # update bounding box
for axis in range(3):
b[axis][0] = min([b[axis][0], _e[axis]])
b[axis][1] = max([b[axis][1], _e[axis]])
column.append(_e)
self._calculating_verts_pos += 1.0
verts.append(column)
for axis in range(3):
b[axis][2] = b[axis][1] - b[axis][0]
if b[axis][2] == 0.0:
b[axis][2] = 1.0
self.verts = verts
self.push_wireframe(self.draw_verts(False, False))
self.push_solid(self.draw_verts(False, True))
def _on_calculate_cverts(self):
if not self.verts or not self.color:
return
def set_work_len(n):
self._calculating_cverts_len = float(n)
def inc_work_pos():
self._calculating_cverts_pos += 1.0
set_work_len(1)
self._calculating_cverts_pos = 0
self.cverts = self.color.apply_to_surface(self.verts,
self.u_set,
self.v_set,
set_len=set_work_len,
inc_pos=inc_work_pos)
self.push_solid(self.draw_verts(True, True))
def calculate_one_cvert(self, u, v):
vert = self.verts[u][v]
return self.color(vert[0], vert[1], vert[2],
self.u_set[u], self.v_set[v])
def draw_verts(self, use_cverts, use_solid_color):
def f():
for u in range(1, len(self.u_set)):
pgl.glBegin(pgl.GL_QUAD_STRIP)
for v in range(len(self.v_set)):
pa = self.verts[u - 1][v]
pb = self.verts[u][v]
if pa is None or pb is None:
pgl.glEnd()
pgl.glBegin(pgl.GL_QUAD_STRIP)
continue
if use_cverts:
ca = self.cverts[u - 1][v]
cb = self.cverts[u][v]
if ca is None:
ca = (0, 0, 0)
if cb is None:
cb = (0, 0, 0)
else:
if use_solid_color:
ca = cb = self.default_solid_color
else:
ca = cb = self.default_wireframe_color
pgl.glColor3f(*ca)
pgl.glVertex3f(*pa)
pgl.glColor3f(*cb)
pgl.glVertex3f(*pb)
pgl.glEnd()
return f

View File

@ -0,0 +1,144 @@
from time import perf_counter
import pyglet.gl as pgl
from sympy.plotting.pygletplot.managed_window import ManagedWindow
from sympy.plotting.pygletplot.plot_camera import PlotCamera
from sympy.plotting.pygletplot.plot_controller import PlotController
class PlotWindow(ManagedWindow):
def __init__(self, plot, antialiasing=True, ortho=False,
invert_mouse_zoom=False, linewidth=1.5, caption="SymPy Plot",
**kwargs):
"""
Named Arguments
===============
antialiasing = True
True OR False
ortho = False
True OR False
invert_mouse_zoom = False
True OR False
"""
self.plot = plot
self.camera = None
self._calculating = False
self.antialiasing = antialiasing
self.ortho = ortho
self.invert_mouse_zoom = invert_mouse_zoom
self.linewidth = linewidth
self.title = caption
self.last_caption_update = 0
self.caption_update_interval = 0.2
self.drawing_first_object = True
super().__init__(**kwargs)
def setup(self):
self.camera = PlotCamera(self, ortho=self.ortho)
self.controller = PlotController(self,
invert_mouse_zoom=self.invert_mouse_zoom)
self.push_handlers(self.controller)
pgl.glClearColor(1.0, 1.0, 1.0, 0.0)
pgl.glClearDepth(1.0)
pgl.glDepthFunc(pgl.GL_LESS)
pgl.glEnable(pgl.GL_DEPTH_TEST)
pgl.glEnable(pgl.GL_LINE_SMOOTH)
pgl.glShadeModel(pgl.GL_SMOOTH)
pgl.glLineWidth(self.linewidth)
pgl.glEnable(pgl.GL_BLEND)
pgl.glBlendFunc(pgl.GL_SRC_ALPHA, pgl.GL_ONE_MINUS_SRC_ALPHA)
if self.antialiasing:
pgl.glHint(pgl.GL_LINE_SMOOTH_HINT, pgl.GL_NICEST)
pgl.glHint(pgl.GL_POLYGON_SMOOTH_HINT, pgl.GL_NICEST)
self.camera.setup_projection()
def on_resize(self, w, h):
super().on_resize(w, h)
if self.camera is not None:
self.camera.setup_projection()
def update(self, dt):
self.controller.update(dt)
def draw(self):
self.plot._render_lock.acquire()
self.camera.apply_transformation()
calc_verts_pos, calc_verts_len = 0, 0
calc_cverts_pos, calc_cverts_len = 0, 0
should_update_caption = (perf_counter() - self.last_caption_update >
self.caption_update_interval)
if len(self.plot._functions.values()) == 0:
self.drawing_first_object = True
iterfunctions = iter(self.plot._functions.values())
for r in iterfunctions:
if self.drawing_first_object:
self.camera.set_rot_preset(r.default_rot_preset)
self.drawing_first_object = False
pgl.glPushMatrix()
r._draw()
pgl.glPopMatrix()
# might as well do this while we are
# iterating and have the lock rather
# than locking and iterating twice
# per frame:
if should_update_caption:
try:
if r.calculating_verts:
calc_verts_pos += r.calculating_verts_pos
calc_verts_len += r.calculating_verts_len
if r.calculating_cverts:
calc_cverts_pos += r.calculating_cverts_pos
calc_cverts_len += r.calculating_cverts_len
except ValueError:
pass
for r in self.plot._pobjects:
pgl.glPushMatrix()
r._draw()
pgl.glPopMatrix()
if should_update_caption:
self.update_caption(calc_verts_pos, calc_verts_len,
calc_cverts_pos, calc_cverts_len)
self.last_caption_update = perf_counter()
if self.plot._screenshot:
self.plot._screenshot._execute_saving()
self.plot._render_lock.release()
def update_caption(self, calc_verts_pos, calc_verts_len,
calc_cverts_pos, calc_cverts_len):
caption = self.title
if calc_verts_len or calc_cverts_len:
caption += " (calculating"
if calc_verts_len > 0:
p = (calc_verts_pos / calc_verts_len) * 100
caption += " vertices %i%%" % (p)
if calc_cverts_len > 0:
p = (calc_cverts_pos / calc_cverts_len) * 100
caption += " colors %i%%" % (p)
caption += ")"
if self.caption != caption:
self.set_caption(caption)

View File

@ -0,0 +1,88 @@
from sympy.external.importtools import import_module
disabled = False
# if pyglet.gl fails to import, e.g. opengl is missing, we disable the tests
pyglet_gl = import_module("pyglet.gl", catch=(OSError,))
pyglet_window = import_module("pyglet.window", catch=(OSError,))
if not pyglet_gl or not pyglet_window:
disabled = True
from sympy.core.symbol import symbols
from sympy.functions.elementary.exponential import log
from sympy.functions.elementary.trigonometric import (cos, sin)
x, y, z = symbols('x, y, z')
def test_plot_2d():
from sympy.plotting.pygletplot import PygletPlot
p = PygletPlot(x, [x, -5, 5, 4], visible=False)
p.wait_for_calculations()
def test_plot_2d_discontinuous():
from sympy.plotting.pygletplot import PygletPlot
p = PygletPlot(1/x, [x, -1, 1, 2], visible=False)
p.wait_for_calculations()
def test_plot_3d():
from sympy.plotting.pygletplot import PygletPlot
p = PygletPlot(x*y, [x, -5, 5, 5], [y, -5, 5, 5], visible=False)
p.wait_for_calculations()
def test_plot_3d_discontinuous():
from sympy.plotting.pygletplot import PygletPlot
p = PygletPlot(1/x, [x, -3, 3, 6], [y, -1, 1, 1], visible=False)
p.wait_for_calculations()
def test_plot_2d_polar():
from sympy.plotting.pygletplot import PygletPlot
p = PygletPlot(1/x, [x, -1, 1, 4], 'mode=polar', visible=False)
p.wait_for_calculations()
def test_plot_3d_cylinder():
from sympy.plotting.pygletplot import PygletPlot
p = PygletPlot(
1/y, [x, 0, 6.282, 4], [y, -1, 1, 4], 'mode=polar;style=solid',
visible=False)
p.wait_for_calculations()
def test_plot_3d_spherical():
from sympy.plotting.pygletplot import PygletPlot
p = PygletPlot(
1, [x, 0, 6.282, 4], [y, 0, 3.141,
4], 'mode=spherical;style=wireframe',
visible=False)
p.wait_for_calculations()
def test_plot_2d_parametric():
from sympy.plotting.pygletplot import PygletPlot
p = PygletPlot(sin(x), cos(x), [x, 0, 6.282, 4], visible=False)
p.wait_for_calculations()
def test_plot_3d_parametric():
from sympy.plotting.pygletplot import PygletPlot
p = PygletPlot(sin(x), cos(x), x/5.0, [x, 0, 6.282, 4], visible=False)
p.wait_for_calculations()
def _test_plot_log():
from sympy.plotting.pygletplot import PygletPlot
p = PygletPlot(log(x), [x, 0, 6.282, 4], 'mode=polar', visible=False)
p.wait_for_calculations()
def test_plot_integral():
# Make sure it doesn't treat x as an independent variable
from sympy.plotting.pygletplot import PygletPlot
from sympy.integrals.integrals import Integral
p = PygletPlot(Integral(z*x, (x, 1, z), (z, 1, y)), visible=False)
p.wait_for_calculations()

View File

@ -0,0 +1,188 @@
try:
from ctypes import c_float, c_int, c_double
except ImportError:
pass
import pyglet.gl as pgl
from sympy.core import S
def get_model_matrix(array_type=c_float, glGetMethod=pgl.glGetFloatv):
"""
Returns the current modelview matrix.
"""
m = (array_type*16)()
glGetMethod(pgl.GL_MODELVIEW_MATRIX, m)
return m
def get_projection_matrix(array_type=c_float, glGetMethod=pgl.glGetFloatv):
"""
Returns the current modelview matrix.
"""
m = (array_type*16)()
glGetMethod(pgl.GL_PROJECTION_MATRIX, m)
return m
def get_viewport():
"""
Returns the current viewport.
"""
m = (c_int*4)()
pgl.glGetIntegerv(pgl.GL_VIEWPORT, m)
return m
def get_direction_vectors():
m = get_model_matrix()
return ((m[0], m[4], m[8]),
(m[1], m[5], m[9]),
(m[2], m[6], m[10]))
def get_view_direction_vectors():
m = get_model_matrix()
return ((m[0], m[1], m[2]),
(m[4], m[5], m[6]),
(m[8], m[9], m[10]))
def get_basis_vectors():
return ((1, 0, 0), (0, 1, 0), (0, 0, 1))
def screen_to_model(x, y, z):
m = get_model_matrix(c_double, pgl.glGetDoublev)
p = get_projection_matrix(c_double, pgl.glGetDoublev)
w = get_viewport()
mx, my, mz = c_double(), c_double(), c_double()
pgl.gluUnProject(x, y, z, m, p, w, mx, my, mz)
return float(mx.value), float(my.value), float(mz.value)
def model_to_screen(x, y, z):
m = get_model_matrix(c_double, pgl.glGetDoublev)
p = get_projection_matrix(c_double, pgl.glGetDoublev)
w = get_viewport()
mx, my, mz = c_double(), c_double(), c_double()
pgl.gluProject(x, y, z, m, p, w, mx, my, mz)
return float(mx.value), float(my.value), float(mz.value)
def vec_subs(a, b):
return tuple(a[i] - b[i] for i in range(len(a)))
def billboard_matrix():
"""
Removes rotational components of
current matrix so that primitives
are always drawn facing the viewer.
|1|0|0|x|
|0|1|0|x|
|0|0|1|x| (x means left unchanged)
|x|x|x|x|
"""
m = get_model_matrix()
# XXX: for i in range(11): m[i] = i ?
m[0] = 1
m[1] = 0
m[2] = 0
m[4] = 0
m[5] = 1
m[6] = 0
m[8] = 0
m[9] = 0
m[10] = 1
pgl.glLoadMatrixf(m)
def create_bounds():
return [[S.Infinity, S.NegativeInfinity, 0],
[S.Infinity, S.NegativeInfinity, 0],
[S.Infinity, S.NegativeInfinity, 0]]
def update_bounds(b, v):
if v is None:
return
for axis in range(3):
b[axis][0] = min([b[axis][0], v[axis]])
b[axis][1] = max([b[axis][1], v[axis]])
def interpolate(a_min, a_max, a_ratio):
return a_min + a_ratio * (a_max - a_min)
def rinterpolate(a_min, a_max, a_value):
a_range = a_max - a_min
if a_max == a_min:
a_range = 1.0
return (a_value - a_min) / float(a_range)
def interpolate_color(color1, color2, ratio):
return tuple(interpolate(color1[i], color2[i], ratio) for i in range(3))
def scale_value(v, v_min, v_len):
return (v - v_min) / v_len
def scale_value_list(flist):
v_min, v_max = min(flist), max(flist)
v_len = v_max - v_min
return [scale_value(f, v_min, v_len) for f in flist]
def strided_range(r_min, r_max, stride, max_steps=50):
o_min, o_max = r_min, r_max
if abs(r_min - r_max) < 0.001:
return []
try:
range(int(r_min - r_max))
except (TypeError, OverflowError):
return []
if r_min > r_max:
raise ValueError("r_min cannot be greater than r_max")
r_min_s = (r_min % stride)
r_max_s = stride - (r_max % stride)
if abs(r_max_s - stride) < 0.001:
r_max_s = 0.0
r_min -= r_min_s
r_max += r_max_s
r_steps = int((r_max - r_min)/stride)
if max_steps and r_steps > max_steps:
return strided_range(o_min, o_max, stride*2)
return [r_min] + [r_min + e*stride for e in range(1, r_steps + 1)] + [r_max]
def parse_option_string(s):
if not isinstance(s, str):
return None
options = {}
for token in s.split(';'):
pieces = token.split('=')
if len(pieces) == 1:
option, value = pieces[0], ""
elif len(pieces) == 2:
option, value = pieces
else:
raise ValueError("Plot option string '%s' is malformed." % (s))
options[option.strip()] = value.strip()
return options
def dot_product(v1, v2):
return sum(v1[i]*v2[i] for i in range(3))
def vec_sub(v1, v2):
return tuple(v1[i] - v2[i] for i in range(3))
def vec_mag(v):
return sum(v[i]**2 for i in range(3))**(0.5)