123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400 |
- 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)
|