from .accumulationbounds import AccumBounds, AccumulationBounds # noqa: F401 from .singularities import singularities from sympy.core import Pow, S from sympy.core.function import diff, expand_mul, Function from sympy.core.kind import NumberKind from sympy.core.mod import Mod from sympy.core.numbers import equal_valued from sympy.core.relational import Relational from sympy.core.symbol import Symbol, Dummy from sympy.core.sympify import _sympify from sympy.functions.elementary.complexes import Abs, im, re from sympy.functions.elementary.exponential import exp, log from sympy.functions.elementary.integers import frac from sympy.functions.elementary.piecewise import Piecewise from sympy.functions.elementary.trigonometric import ( TrigonometricFunction, sin, cos, tan, cot, csc, sec, asin, acos, acot, atan, asec, acsc) from sympy.functions.elementary.hyperbolic import (sinh, cosh, tanh, coth, sech, csch, asinh, acosh, atanh, acoth, asech, acsch) from sympy.polys.polytools import degree, lcm_list from sympy.sets.sets import (Interval, Intersection, FiniteSet, Union, Complement) from sympy.sets.fancysets import ImageSet from sympy.sets.conditionset import ConditionSet from sympy.utilities import filldedent from sympy.utilities.iterables import iterable from sympy.matrices.dense import hessian def continuous_domain(f, symbol, domain): """ Returns the domain on which the function expression f is continuous. This function is limited by the ability to determine the various singularities and discontinuities of the given function. The result is either given as a union of intervals or constructed using other set operations. Parameters ========== f : :py:class:`~.Expr` The concerned function. symbol : :py:class:`~.Symbol` The variable for which the intervals are to be determined. domain : :py:class:`~.Interval` The domain over which the continuity of the symbol has to be checked. Examples ======== >>> from sympy import Interval, Symbol, S, tan, log, pi, sqrt >>> from sympy.calculus.util import continuous_domain >>> x = Symbol('x') >>> continuous_domain(1/x, x, S.Reals) Union(Interval.open(-oo, 0), Interval.open(0, oo)) >>> continuous_domain(tan(x), x, Interval(0, pi)) Union(Interval.Ropen(0, pi/2), Interval.Lopen(pi/2, pi)) >>> continuous_domain(sqrt(x - 2), x, Interval(-5, 5)) Interval(2, 5) >>> continuous_domain(log(2*x - 1), x, S.Reals) Interval.open(1/2, oo) Returns ======= :py:class:`~.Interval` Union of all intervals where the function is continuous. Raises ====== NotImplementedError If the method to determine continuity of such a function has not yet been developed. """ from sympy.solvers.inequalities import solve_univariate_inequality if not domain.is_subset(S.Reals): raise NotImplementedError(filldedent(''' Domain must be a subset of S.Reals. ''')) implemented = [Pow, exp, log, Abs, frac, sin, cos, tan, cot, sec, csc, asin, acos, atan, acot, asec, acsc, sinh, cosh, tanh, coth, sech, csch, asinh, acosh, atanh, acoth, asech, acsch] used = [fct.func for fct in f.atoms(Function) if fct.has(symbol)] if any(func not in implemented for func in used): raise NotImplementedError(filldedent(''' Unable to determine the domain of the given function. ''')) x = Symbol('x') constraints = { log: (x > 0,), asin: (x >= -1, x <= 1), acos: (x >= -1, x <= 1), acosh: (x >= 1,), atanh: (x > -1, x < 1), asech: (x > 0, x <= 1) } constraints_union = { asec: (x <= -1, x >= 1), acsc: (x <= -1, x >= 1), acoth: (x < -1, x > 1) } cont_domain = domain for atom in f.atoms(Pow): den = atom.exp.as_numer_denom()[1] if atom.exp.is_rational and den.is_odd: pass # 0**negative handled by singularities() else: constraint = solve_univariate_inequality(atom.base >= 0, symbol).as_set() cont_domain = Intersection(constraint, cont_domain) for atom in f.atoms(Function): if atom.func in constraints: for c in constraints[atom.func]: constraint_relational = c.subs(x, atom.args[0]) constraint_set = solve_univariate_inequality( constraint_relational, symbol).as_set() cont_domain = Intersection(constraint_set, cont_domain) elif atom.func in constraints_union: constraint_set = S.EmptySet for c in constraints_union[atom.func]: constraint_relational = c.subs(x, atom.args[0]) constraint_set += solve_univariate_inequality( constraint_relational, symbol).as_set() cont_domain = Intersection(constraint_set, cont_domain) # XXX: the discontinuities below could be factored out in # a new "discontinuities()". elif atom.func == acot: from sympy.solvers.solveset import solveset_real # Sympy's acot() has a step discontinuity at 0. Since it's # neither an essential singularity nor a pole, singularities() # will not report it. But it's still relevant for determining # the continuity of the function f. cont_domain -= solveset_real(atom.args[0], symbol) # Note that the above may introduce spurious discontinuities, e.g. # for abs(acot(x)) at 0. elif atom.func == frac: from sympy.solvers.solveset import solveset_real r = function_range(atom.args[0], symbol, domain) r = Intersection(r, S.Integers) if r.is_finite_set: discont = S.EmptySet for n in r: discont += solveset_real(atom.args[0]-n, symbol) else: discont = ConditionSet( symbol, S.Integers.contains(atom.args[0]), cont_domain) cont_domain -= discont return cont_domain - singularities(f, symbol, domain) def function_range(f, symbol, domain): """ Finds the range of a function in a given domain. This method is limited by the ability to determine the singularities and determine limits. Parameters ========== f : :py:class:`~.Expr` The concerned function. symbol : :py:class:`~.Symbol` The variable for which the range of function is to be determined. domain : :py:class:`~.Interval` The domain under which the range of the function has to be found. Examples ======== >>> from sympy import Interval, Symbol, S, exp, log, pi, sqrt, sin, tan >>> from sympy.calculus.util import function_range >>> x = Symbol('x') >>> function_range(sin(x), x, Interval(0, 2*pi)) Interval(-1, 1) >>> function_range(tan(x), x, Interval(-pi/2, pi/2)) Interval(-oo, oo) >>> function_range(1/x, x, S.Reals) Union(Interval.open(-oo, 0), Interval.open(0, oo)) >>> function_range(exp(x), x, S.Reals) Interval.open(0, oo) >>> function_range(log(x), x, S.Reals) Interval(-oo, oo) >>> function_range(sqrt(x), x, Interval(-5, 9)) Interval(0, 3) Returns ======= :py:class:`~.Interval` Union of all ranges for all intervals under domain where function is continuous. Raises ====== NotImplementedError If any of the intervals, in the given domain, for which function is continuous are not finite or real, OR if the critical points of the function on the domain cannot be found. """ if domain is S.EmptySet: return S.EmptySet period = periodicity(f, symbol) if period == S.Zero: # the expression is constant wrt symbol return FiniteSet(f.expand()) from sympy.series.limits import limit from sympy.solvers.solveset import solveset if period is not None: if isinstance(domain, Interval): if (domain.inf - domain.sup).is_infinite: domain = Interval(0, period) elif isinstance(domain, Union): for sub_dom in domain.args: if isinstance(sub_dom, Interval) and \ ((sub_dom.inf - sub_dom.sup).is_infinite): domain = Interval(0, period) intervals = continuous_domain(f, symbol, domain) range_int = S.EmptySet if isinstance(intervals,(Interval, FiniteSet)): interval_iter = (intervals,) elif isinstance(intervals, Union): interval_iter = intervals.args else: raise NotImplementedError(filldedent(''' Unable to find range for the given domain. ''')) for interval in interval_iter: if isinstance(interval, FiniteSet): for singleton in interval: if singleton in domain: range_int += FiniteSet(f.subs(symbol, singleton)) elif isinstance(interval, Interval): vals = S.EmptySet critical_points = S.EmptySet critical_values = S.EmptySet bounds = ((interval.left_open, interval.inf, '+'), (interval.right_open, interval.sup, '-')) for is_open, limit_point, direction in bounds: if is_open: critical_values += FiniteSet(limit(f, symbol, limit_point, direction)) vals += critical_values else: vals += FiniteSet(f.subs(symbol, limit_point)) solution = solveset(f.diff(symbol), symbol, interval) if not iterable(solution): raise NotImplementedError( 'Unable to find critical points for {}'.format(f)) if isinstance(solution, ImageSet): raise NotImplementedError( 'Infinite number of critical points for {}'.format(f)) critical_points += solution for critical_point in critical_points: vals += FiniteSet(f.subs(symbol, critical_point)) left_open, right_open = False, False if critical_values is not S.EmptySet: if critical_values.inf == vals.inf: left_open = True if critical_values.sup == vals.sup: right_open = True range_int += Interval(vals.inf, vals.sup, left_open, right_open) else: raise NotImplementedError(filldedent(''' Unable to find range for the given domain. ''')) return range_int def not_empty_in(finset_intersection, *syms): """ Finds the domain of the functions in ``finset_intersection`` in which the ``finite_set`` is not-empty. Parameters ========== finset_intersection : Intersection of FiniteSet The unevaluated intersection of FiniteSet containing real-valued functions with Union of Sets syms : Tuple of symbols Symbol for which domain is to be found Raises ====== NotImplementedError The algorithms to find the non-emptiness of the given FiniteSet are not yet implemented. ValueError The input is not valid. RuntimeError It is a bug, please report it to the github issue tracker (https://github.com/sympy/sympy/issues). Examples ======== >>> from sympy import FiniteSet, Interval, not_empty_in, oo >>> from sympy.abc import x >>> not_empty_in(FiniteSet(x/2).intersect(Interval(0, 1)), x) Interval(0, 2) >>> not_empty_in(FiniteSet(x, x**2).intersect(Interval(1, 2)), x) Union(Interval(1, 2), Interval(-sqrt(2), -1)) >>> not_empty_in(FiniteSet(x**2/(x + 2)).intersect(Interval(1, oo)), x) Union(Interval.Lopen(-2, -1), Interval(2, oo)) """ # TODO: handle piecewise defined functions # TODO: handle transcendental functions # TODO: handle multivariate functions if len(syms) == 0: raise ValueError("One or more symbols must be given in syms.") if finset_intersection is S.EmptySet: return S.EmptySet if isinstance(finset_intersection, Union): elm_in_sets = finset_intersection.args[0] return Union(not_empty_in(finset_intersection.args[1], *syms), elm_in_sets) if isinstance(finset_intersection, FiniteSet): finite_set = finset_intersection _sets = S.Reals else: finite_set = finset_intersection.args[1] _sets = finset_intersection.args[0] if not isinstance(finite_set, FiniteSet): raise ValueError('A FiniteSet must be given, not %s: %s' % (type(finite_set), finite_set)) if len(syms) == 1: symb = syms[0] else: raise NotImplementedError('more than one variables %s not handled' % (syms,)) def elm_domain(expr, intrvl): """ Finds the domain of an expression in any given interval """ from sympy.solvers.solveset import solveset _start = intrvl.start _end = intrvl.end _singularities = solveset(expr.as_numer_denom()[1], symb, domain=S.Reals) if intrvl.right_open: if _end is S.Infinity: _domain1 = S.Reals else: _domain1 = solveset(expr < _end, symb, domain=S.Reals) else: _domain1 = solveset(expr <= _end, symb, domain=S.Reals) if intrvl.left_open: if _start is S.NegativeInfinity: _domain2 = S.Reals else: _domain2 = solveset(expr > _start, symb, domain=S.Reals) else: _domain2 = solveset(expr >= _start, symb, domain=S.Reals) # domain in the interval expr_with_sing = Intersection(_domain1, _domain2) expr_domain = Complement(expr_with_sing, _singularities) return expr_domain if isinstance(_sets, Interval): return Union(*[elm_domain(element, _sets) for element in finite_set]) if isinstance(_sets, Union): _domain = S.EmptySet for intrvl in _sets.args: _domain_element = Union(*[elm_domain(element, intrvl) for element in finite_set]) _domain = Union(_domain, _domain_element) return _domain def periodicity(f, symbol, check=False): """ Tests the given function for periodicity in the given symbol. Parameters ========== f : :py:class:`~.Expr` The concerned function. symbol : :py:class:`~.Symbol` The variable for which the period is to be determined. check : bool, optional The flag to verify whether the value being returned is a period or not. Returns ======= period The period of the function is returned. ``None`` is returned when the function is aperiodic or has a complex period. The value of $0$ is returned as the period of a constant function. Raises ====== NotImplementedError The value of the period computed cannot be verified. Notes ===== Currently, we do not support functions with a complex period. The period of functions having complex periodic values such as ``exp``, ``sinh`` is evaluated to ``None``. The value returned might not be the "fundamental" period of the given function i.e. it may not be the smallest periodic value of the function. The verification of the period through the ``check`` flag is not reliable due to internal simplification of the given expression. Hence, it is set to ``False`` by default. Examples ======== >>> from sympy import periodicity, Symbol, sin, cos, tan, exp >>> x = Symbol('x') >>> f = sin(x) + sin(2*x) + sin(3*x) >>> periodicity(f, x) 2*pi >>> periodicity(sin(x)*cos(x), x) pi >>> periodicity(exp(tan(2*x) - 1), x) pi/2 >>> periodicity(sin(4*x)**cos(2*x), x) pi >>> periodicity(exp(x), x) """ if symbol.kind is not NumberKind: raise NotImplementedError("Cannot use symbol of kind %s" % symbol.kind) temp = Dummy('x', real=True) f = f.subs(symbol, temp) symbol = temp def _check(orig_f, period): '''Return the checked period or raise an error.''' new_f = orig_f.subs(symbol, symbol + period) if new_f.equals(orig_f): return period else: raise NotImplementedError(filldedent(''' The period of the given function cannot be verified. When `%s` was replaced with `%s + %s` in `%s`, the result was `%s` which was not recognized as being the same as the original function. So either the period was wrong or the two forms were not recognized as being equal. Set check=False to obtain the value.''' % (symbol, symbol, period, orig_f, new_f))) orig_f = f period = None if isinstance(f, Relational): f = f.lhs - f.rhs f = f.simplify() if symbol not in f.free_symbols: return S.Zero if isinstance(f, TrigonometricFunction): try: period = f.period(symbol) except NotImplementedError: pass if isinstance(f, Abs): arg = f.args[0] if isinstance(arg, (sec, csc, cos)): # all but tan and cot might have a # a period that is half as large # so recast as sin arg = sin(arg.args[0]) period = periodicity(arg, symbol) if period is not None and isinstance(arg, sin): # the argument of Abs was a trigonometric other than # cot or tan; test to see if the half-period # is valid. Abs(arg) has behaviour equivalent to # orig_f, so use that for test: orig_f = Abs(arg) try: return _check(orig_f, period/2) except NotImplementedError as err: if check: raise NotImplementedError(err) # else let new orig_f and period be # checked below if isinstance(f, exp) or (f.is_Pow and f.base == S.Exp1): f = Pow(S.Exp1, expand_mul(f.exp)) if im(f) != 0: period_real = periodicity(re(f), symbol) period_imag = periodicity(im(f), symbol) if period_real is not None and period_imag is not None: period = lcim([period_real, period_imag]) if f.is_Pow and f.base != S.Exp1: base, expo = f.args base_has_sym = base.has(symbol) expo_has_sym = expo.has(symbol) if base_has_sym and not expo_has_sym: period = periodicity(base, symbol) elif expo_has_sym and not base_has_sym: period = periodicity(expo, symbol) else: period = _periodicity(f.args, symbol) elif f.is_Mul: coeff, g = f.as_independent(symbol, as_Add=False) if isinstance(g, TrigonometricFunction) or not equal_valued(coeff, 1): period = periodicity(g, symbol) else: period = _periodicity(g.args, symbol) elif f.is_Add: k, g = f.as_independent(symbol) if k is not S.Zero: return periodicity(g, symbol) period = _periodicity(g.args, symbol) elif isinstance(f, Mod): a, n = f.args if a == symbol: period = n elif isinstance(a, TrigonometricFunction): period = periodicity(a, symbol) #check if 'f' is linear in 'symbol' elif (a.is_polynomial(symbol) and degree(a, symbol) == 1 and symbol not in n.free_symbols): period = Abs(n / a.diff(symbol)) elif isinstance(f, Piecewise): pass # not handling Piecewise yet as the return type is not favorable elif period is None: from sympy.solvers.decompogen import compogen, decompogen g_s = decompogen(f, symbol) num_of_gs = len(g_s) if num_of_gs > 1: for index, g in enumerate(reversed(g_s)): start_index = num_of_gs - 1 - index g = compogen(g_s[start_index:], symbol) if g not in (orig_f, f): # Fix for issue 12620 period = periodicity(g, symbol) if period is not None: break if period is not None: if check: return _check(orig_f, period) return period return None def _periodicity(args, symbol): """ Helper for `periodicity` to find the period of a list of simpler functions. It uses the `lcim` method to find the least common period of all the functions. Parameters ========== args : Tuple of :py:class:`~.Symbol` All the symbols present in a function. symbol : :py:class:`~.Symbol` The symbol over which the function is to be evaluated. Returns ======= period The least common period of the function for all the symbols of the function. ``None`` if for at least one of the symbols the function is aperiodic. """ periods = [] for f in args: period = periodicity(f, symbol) if period is None: return None if period is not S.Zero: periods.append(period) if len(periods) > 1: return lcim(periods) if periods: return periods[0] def lcim(numbers): """Returns the least common integral multiple of a list of numbers. The numbers can be rational or irrational or a mixture of both. `None` is returned for incommensurable numbers. Parameters ========== numbers : list Numbers (rational and/or irrational) for which lcim is to be found. Returns ======= number lcim if it exists, otherwise ``None`` for incommensurable numbers. Examples ======== >>> from sympy.calculus.util import lcim >>> from sympy import S, pi >>> lcim([S(1)/2, S(3)/4, S(5)/6]) 15/2 >>> lcim([2*pi, 3*pi, pi, pi/2]) 6*pi >>> lcim([S(1), 2*pi]) """ result = None if all(num.is_irrational for num in numbers): factorized_nums = [num.factor() for num in numbers] factors_num = [num.as_coeff_Mul() for num in factorized_nums] term = factors_num[0][1] if all(factor == term for coeff, factor in factors_num): common_term = term coeffs = [coeff for coeff, factor in factors_num] result = lcm_list(coeffs) * common_term elif all(num.is_rational for num in numbers): result = lcm_list(numbers) else: pass return result def is_convex(f, *syms, domain=S.Reals): r"""Determines the convexity of the function passed in the argument. Parameters ========== f : :py:class:`~.Expr` The concerned function. syms : Tuple of :py:class:`~.Symbol` The variables with respect to which the convexity is to be determined. domain : :py:class:`~.Interval`, optional The domain over which the convexity of the function has to be checked. If unspecified, S.Reals will be the default domain. Returns ======= bool The method returns ``True`` if the function is convex otherwise it returns ``False``. Raises ====== NotImplementedError The check for the convexity of multivariate functions is not implemented yet. Notes ===== To determine concavity of a function pass `-f` as the concerned function. To determine logarithmic convexity of a function pass `\log(f)` as concerned function. To determine logarithmic concavity of a function pass `-\log(f)` as concerned function. Currently, convexity check of multivariate functions is not handled. Examples ======== >>> from sympy import is_convex, symbols, exp, oo, Interval >>> x = symbols('x') >>> is_convex(exp(x), x) True >>> is_convex(x**3, x, domain = Interval(-1, oo)) False >>> is_convex(1/x**2, x, domain=Interval.open(0, oo)) True References ========== .. [1] https://en.wikipedia.org/wiki/Convex_function .. [2] http://www.ifp.illinois.edu/~angelia/L3_convfunc.pdf .. [3] https://en.wikipedia.org/wiki/Logarithmically_convex_function .. [4] https://en.wikipedia.org/wiki/Logarithmically_concave_function .. [5] https://en.wikipedia.org/wiki/Concave_function """ if len(syms) > 1 : return hessian(f, syms).is_positive_semidefinite from sympy.solvers.inequalities import solve_univariate_inequality f = _sympify(f) var = syms[0] if any(s in domain for s in singularities(f, var)): return False condition = f.diff(var, 2) < 0 if solve_univariate_inequality(condition, var, False, domain): return False return True def stationary_points(f, symbol, domain=S.Reals): """ Returns the stationary points of a function (where derivative of the function is 0) in the given domain. Parameters ========== f : :py:class:`~.Expr` The concerned function. symbol : :py:class:`~.Symbol` The variable for which the stationary points are to be determined. domain : :py:class:`~.Interval` The domain over which the stationary points have to be checked. If unspecified, ``S.Reals`` will be the default domain. Returns ======= Set A set of stationary points for the function. If there are no stationary point, an :py:class:`~.EmptySet` is returned. Examples ======== >>> from sympy import Interval, Symbol, S, sin, pi, pprint, stationary_points >>> x = Symbol('x') >>> stationary_points(1/x, x, S.Reals) EmptySet >>> pprint(stationary_points(sin(x), x), use_unicode=False) pi 3*pi {2*n*pi + -- | n in Integers} U {2*n*pi + ---- | n in Integers} 2 2 >>> stationary_points(sin(x),x, Interval(0, 4*pi)) {pi/2, 3*pi/2, 5*pi/2, 7*pi/2} """ from sympy.solvers.solveset import solveset if domain is S.EmptySet: return S.EmptySet domain = continuous_domain(f, symbol, domain) set = solveset(diff(f, symbol), symbol, domain) return set def maximum(f, symbol, domain=S.Reals): """ Returns the maximum value of a function in the given domain. Parameters ========== f : :py:class:`~.Expr` The concerned function. symbol : :py:class:`~.Symbol` The variable for maximum value needs to be determined. domain : :py:class:`~.Interval` The domain over which the maximum have to be checked. If unspecified, then the global maximum is returned. Returns ======= number Maximum value of the function in given domain. Examples ======== >>> from sympy import Interval, Symbol, S, sin, cos, pi, maximum >>> x = Symbol('x') >>> f = -x**2 + 2*x + 5 >>> maximum(f, x, S.Reals) 6 >>> maximum(sin(x), x, Interval(-pi, pi/4)) sqrt(2)/2 >>> maximum(sin(x)*cos(x), x) 1/2 """ if isinstance(symbol, Symbol): if domain is S.EmptySet: raise ValueError("Maximum value not defined for empty domain.") return function_range(f, symbol, domain).sup else: raise ValueError("%s is not a valid symbol." % symbol) def minimum(f, symbol, domain=S.Reals): """ Returns the minimum value of a function in the given domain. Parameters ========== f : :py:class:`~.Expr` The concerned function. symbol : :py:class:`~.Symbol` The variable for minimum value needs to be determined. domain : :py:class:`~.Interval` The domain over which the minimum have to be checked. If unspecified, then the global minimum is returned. Returns ======= number Minimum value of the function in the given domain. Examples ======== >>> from sympy import Interval, Symbol, S, sin, cos, minimum >>> x = Symbol('x') >>> f = x**2 + 2*x + 5 >>> minimum(f, x, S.Reals) 4 >>> minimum(sin(x), x, Interval(2, 3)) sin(3) >>> minimum(sin(x)*cos(x), x) -1/2 """ if isinstance(symbol, Symbol): if domain is S.EmptySet: raise ValueError("Minimum value not defined for empty domain.") return function_range(f, symbol, domain).inf else: raise ValueError("%s is not a valid symbol." % symbol)