Spaces:
Sleeping
Sleeping
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 | |
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") | |
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)) | |
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)) | |
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)))) | |
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 | |
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]) | |
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 | |
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) | |