def test_numpoly_ndpoly(): poly = numpoly.ndpoly(exponents=[(1, )], shape=(), names="X") poly["<"] = 1 assert poly == X poly = numpoly.ndpoly(exponents=[(1, )], shape=(), names=X) poly["<"] = 1 assert poly == X poly = numpoly.ndpoly(exponents=[(1, 0), (0, 1)], shape=(), names=("X", "Y")) poly["<;"] = 2 poly[";<"] = 3 assert poly == 2 * X + 3 * Y poly = numpoly.ndpoly(exponents=[(1, 0), (0, 1)], shape=(2, ), names="Q") poly["<;"] = [1, 0] poly[";<"] = [0, 1] assert numpy.all(poly == numpoly.symbols("Q0 Q1"))
def ediff1d( ary: PolyLike, to_end: Optional[PolyLike] = None, to_begin: Optional[PolyLike] = None, ) -> ndpoly: """ Difference between consecutive elements of an array. Args: ary: If necessary, will be flattened before the differences are taken. to_end: Polynomial(s) to append at the end of the returned differences. to_begin: Polynomial(s) to prepend at the beginning of the returned differences. Returns: The differences. Loosely, this is ``ary.flat[1:] - ary.flat[:-1]``. Examples: >>> poly = numpoly.monomial(4) >>> poly polynomial([1, q0, q0**2, q0**3]) >>> numpoly.ediff1d(poly) polynomial([q0-1, q0**2-q0, q0**3-q0**2]) >>> q0, q1 = numpoly.variable(2) >>> numpoly.ediff1d(poly, to_begin=q0, to_end=[1, q1]) polynomial([q0, q0-1, q0**2-q0, q0**3-q0**2, 1, q1]) """ ary = numpoly.aspolynomial(ary).ravel() arys_ = [ary[1:] - ary[:-1]] if to_end is not None: arys_.append(numpoly.aspolynomial(to_end).ravel()) if to_begin is not None: arys_.insert(0, numpoly.aspolynomial(to_begin).ravel()) arys = tuple(numpoly.aspolynomial(ary) for ary in arys_) if len(arys) > 1: arys = numpoly.align_exponents(*arys) out = numpoly.ndpoly( exponents=arys[0].exponents, shape=(sum([ary.size for ary in arys]), ), names=arys[0].names, dtype=ary[0].dtype, ) idx = 0 for ary in arys: for key in ary.keys: out.values[key][idx:idx + ary.size] = ary.values[key] idx += ary.size return out
def test_ndpoly(): poly = numpoly.ndpoly(exponents=[(1, )], shape=(), names="q0") poly["<"] = 1 assert poly == X poly = numpoly.ndpoly(exponents=[(1, )], shape=(), names=X) poly["<"] = 1 assert poly == X poly = numpoly.ndpoly(exponents=[(1, 0), (0, 1)], shape=(), names=("q0", "q1")) poly["<;"] = 2 poly[";<"] = 3 assert poly == 2 * X + 3 * Y with numpoly.global_options(varname_filter=r"Q\d+"): poly = numpoly.ndpoly(exponents=[(1, 0), (0, 1)], shape=(2, ), names="Q:2") poly["<;"] = [1, 0] poly[";<"] = [0, 1] assert numpy.all(poly == numpoly.symbols("Q0 Q1"))
def simple_dispatch( numpy_func, inputs, out=None, **kwargs ): """ Dispatch function between numpy and numpoly. Assumes that evaluation can be performed on the coefficients alone and that there are no change to the polynomials. Args: numpy_func (Callable): The numpy function to evaluate `inputs` on. inputs (Iterable[numpoly.ndpoly]): One or more input arrays. out (Optional[numpy.ndarray]): A location into which the result is stored. If provided, it must have a shape that the inputs broadcast to. If not provided or `None`, a freshly-allocated array is returned. A tuple (possible only as a keyword argument) must have length equal to the number of outputs. kwargs: Keyword args passed to `numpy_func`. Returns: (numpoly.ndpoly): Polynomial, where the coefficients from `input` are passed to `numpy_func` to create the output coefficients. """ inputs = numpoly.align_polynomials(*inputs) no_output = out is None for key in inputs[0].keys: if out is None: tmp = numpy_func(*[poly[key] for poly in inputs], **kwargs) out = numpoly.ndpoly( exponents=inputs[0].exponents, shape=tmp.shape, names=inputs[0].indeterminants, dtype=tmp.dtype, ) out[key] = tmp else: tmp = numpy_func( *[poly[key] for poly in inputs], out=out[key], **kwargs) if no_output: out = numpoly.clean_attributes(out) return out
def simple_dispatch(numpy_func: Callable, inputs: Sequence[Any], out: Optional[Tuple[ndpoly, ...]] = None, **kwargs: Any) -> ndpoly: """ Dispatch function between numpy and numpoly. Assumes that evaluation can be performed on the coefficients alone and that there are no change to the polynomials. Args: numpy_func: The numpy function to evaluate `inputs` on. inputs: One or more input arrays. out: A location into which the result is stored. If provided, it must have a shape that the inputs broadcast to. If not provided or `None`, a freshly-allocated array is returned. A tuple (possible only as a keyword argument) must have length equal to the number of outputs. kwargs: Keyword args passed to `numpy_func`. Returns: Polynomial, where the coefficients from `input` are passed to `numpy_func` to create the output coefficients. """ inputs = numpoly.align_polynomials(*inputs) keys = (inputs[0] if out is None else numpoly.aspolynomial(out[0])).keys tmp = numpy_func(*[poly.values[keys[0]] for poly in inputs], **kwargs) if out is None: out_ = numpoly.ndpoly( exponents=inputs[0].exponents, shape=tmp.shape, names=inputs[0].indeterminants, dtype=tmp.dtype, ) else: assert len(out) == 1 out_ = out[0] out_.values[keys[0]] = tmp for key in keys[1:]: out_.values[key] = numpy_func(*[poly.values[key] for poly in inputs], **kwargs) if out is None: out_ = numpoly.clean_attributes(out_) return numpoly.aspolynomial(out_)
def test_floor_divide(interface): """Tests for numpoly.floor_divide.""" poly = polynomial([[0., 2. * Y], [X, 2.]]) assert_equal(interface.floor_divide(poly, 2), [[0., Y], [0., 1]]) assert_equal(interface.floor_divide(poly, [1., 2.]), [[0., Y], [X, 1]]) assert_equal(interface.floor_divide(poly, [[1., 2.], [2., 1.]]), [[0., Y], [0., 2.]]) with raises(numpoly.FeatureNotSupported): interface.floor_divide(poly, X) with raises(numpoly.FeatureNotSupported): poly.__floordiv__(poly) with raises(numpoly.FeatureNotSupported): poly.__rfloordiv__(poly) out = numpoly.ndpoly( exponents=poly.exponents, shape=(2, 2), names=("q0", "q1"), dtype=float, ) numpoly.floor_divide(poly, 2, out=out) assert_equal(out, [[0., Y], [0., 1.]])
def diff( a: PolyLike, n: int = 1, axis: int = -1, prepend: Optional[PolyLike] = None, append: Optional[PolyLike] = None, ) -> ndpoly: """ Calculate the n-th discrete difference along the given axis. The first difference is given by ``out[i] = a[i+1] - a[i]`` along the given axis, higher differences are calculated by using `diff` recursively. Args: a: Input array n: The number of times values are "differenced". If zero, the input is returned as-is. axis: The axis along which the difference is taken, default is the last axis. prepend, append: Values to prepend or append to `a` along axis prior to performing the difference. Scalar values are expanded to arrays with length 1 in the direction of axis and the shape of the input array in along all other axes. Otherwise the dimension and shape must match `a` except along axis. Returns: The n-th differences. The shape of the output is the same as `a` except along `axis` where the dimension is smaller by `n`. The type of the output is the same as the type of the difference between any two elements of `a`. This is the same as the type of `a` in most cases. Examples: >>> q0, q1 = numpoly.variable(2) >>> poly = numpoly.polynomial([1, q0, q1, q0**2, q1-1]) >>> numpoly.diff(poly) polynomial([q0-1, q1-q0, q0**2-q1, -q0**2+q1-1]) >>> numpoly.diff(poly, n=2) polynomial([q1-2*q0+1, q0**2-2*q1+q0, -2*q0**2+2*q1-1]) >>> poly = numpoly.polynomial([[q0, 1], [2, q1]]) >>> numpoly.diff(poly) polynomial([[-q0+1], [q1-2]]) >>> numpoly.diff(poly, prepend=7, append=q1) polynomial([[q0-7, -q0+1, q1-1], [-5, q1-2, 0]]) """ if append is not None: if prepend is not None: a, append, prepend = numpoly.align_exponents(a, append, prepend) else: a, append = numpoly.align_exponents(a, append) elif prepend is not None: a, prepend = numpoly.align_exponents(a, prepend) else: a = numpoly.aspolynomial(a) out = None for key in a.keys: kwargs = {} if append is not None: kwargs["append"] = append.values[key] if prepend is not None: kwargs["prepend"] = prepend.values[key] tmp = numpy.diff(a.values[key], n=n, axis=axis, **kwargs) if out is None: out = numpoly.ndpoly( exponents=a.exponents, shape=tmp.shape, names=a.indeterminants, dtype=tmp.dtype, ) out.values[key] = tmp assert out is not None return numpoly.clean_attributes(out)
def divide(x1, x2, out=None, where=True, **kwargs): """ Return a true division of the inputs, element-wise. Instead of the Python traditional 'floor division', this returns a true division. True division adjusts the output type to present the best answer, regardless of input types. Args: x1 (numpoly.ndpoly): Dividend array. x2 (numpoly.ndpoly): Divisor array. If ``x1.shape != x2.shape``, they must be broadcastable to a commo n shape (which becomes the shape of the output). out (Optional[numpy.ndarray]): A location into which the result is stored. If provided, it must have a shape that the inputs broadcast to. If not provided or `None`, a freshly-allocated array is returned. A tuple (possible only as a keyword argument) must have length equal to the number of outputs. where (Optional[numpy.ndarray]): This condition is broadcast over the input. At locations where the condition is True, the `out` array will be set to the ufunc result. Elsewhere, the `out` array will retain its original value. Note that if an uninitialized `out` array is created via the default ``out=None``, locations within it where the condition is False will remain uninitialized. kwargs: Keyword args passed to numpy.ufunc. Returns: (numpoly.ndpoly): This is a scalar if both `x1` and `x2` are scalars. Examples: >>> xyz = numpoly.symbols("x y z") >>> numpoly.divide(xyz, 4) polynomial([0.25*x, 0.25*y, 0.25*z]) >>> numpoly.divide(xyz, [1, 2, 4]) polynomial([x, 0.5*y, 0.25*z]) >>> numpoly.divide([1, 2, 4], xyz) Traceback (most recent call last): ... ValueError: only constant polynomials can be converted to array. """ x1, x2 = numpoly.align_polynomials(x1, x2) x2 = x2.tonumpy() no_output = out is None if no_output: out = numpoly.ndpoly( exponents=x1.exponents, shape=x1.shape, names=x1.indeterminants, dtype=numpy.common_type(x1, numpy.array(1.)), ) elif not isinstance(out, numpy.ndarray): assert len(out) == 1, "only one output" out = out[0] for key in x1.keys: out[key] = 0 numpy.true_divide(x1[key], x2, out=out[key], where=where, **kwargs) if no_output: out = numpoly.clean_attributes(out) return out
def compose_polynomial_array( arrays, dtype=None, ): """ Compose polynomial from array of arrays of polynomials. Backend for `numpoly.polynomial` when input is undetermined. Args: arrays (Any): Input to be converted to a `numpoly.ndpoly` polynomial type. dtype (Optional[numpy.dtype]): Data type used for the polynomial coefficients. Return: (numpoly.ndpoly): Polynomial based on input `arrays`. """ arrays_ = numpy.array(arrays, dtype=object) shape = arrays_.shape if not arrays_.size: return numpoly.ndpoly(shape=shape, dtype=dtype) if len(arrays_.shape) > 1: return numpoly.concatenate([ numpoly.aspolynomial(array, dtype)[numpy.newaxis] for array in arrays ], axis=0) arrays = arrays_.flatten() indices = numpy.array( [isinstance(array, numpoly.ndpoly) for array in arrays]) arrays[indices] = numpoly.align_indeterminants(*arrays[indices]) names = arrays[indices][0] if numpy.any(indices) else None arrays = arrays.tolist() dtypes = [] keys = {(0, )} for array in arrays: if isinstance(array, numpoly.ndpoly): dtypes.append(array.dtype) keys = keys.union([tuple(key) for key in array.exponents.tolist()]) elif isinstance(array, (numpy.generic, numpy.ndarray)): dtypes.append(array.dtype) else: dtypes.append(type(array)) if dtype is None: dtype = numpy.find_common_type(dtypes, []) length = max([len(key) for key in keys]) collection = {} for idx, array in enumerate(arrays): if isinstance(array, numpoly.ndpoly): for key, value in zip(array.exponents, array.coefficients): key = tuple(key) + (0, ) * (length - len(key)) if key not in collection: collection[key] = numpy.zeros(len(arrays), dtype=dtype) collection[key][idx] = value else: key = (0, ) * length if key not in collection: collection[key] = numpy.zeros(len(arrays), dtype=dtype) collection[key][idx] = array exponents = sorted(collection) coefficients = numpy.array([collection[key] for key in exponents]) coefficients = coefficients.reshape(-1, *shape) exponents, coefficients, names = postprocess_attributes( exponents, coefficients, names) return numpoly.ndpoly.from_attributes( exponents=exponents, coefficients=coefficients, names=names, )
def polynomial( poly_like=None, names=None, dtype=None, ): """ Attempt to cast an object into a polynomial array. Supports various casting options: ================== ======================================================= ``dict`` Keys are tuples that represent polynomial exponents, and values are numpy arrays that represents polynomial coefficients. ``numpoly.ndpoly`` Copy of the polynomial. ``numpy.ndarray`` Constant term polynomial. ``sympy.Poly`` Convert polynomial from ``sympy`` to ``numpoly``, if possible. ``Iterable`` Multivariate array construction. structured array Assumes that the input are raw polynomial core and can be used to construct a polynomial without changing the data. Used for developer convenience. ================== ======================================================= Args: poly_like (typing.Any): Input to be converted to a `numpoly.ndpoly` polynomial type. names (str, typing.Tuple[str, ...]): Name of the indeterminant variables. If possible to infer from ``poly_like``, this argument will be ignored. dtype (type, numpy.dtype): Data type used for the polynomial coefficients. Returns: (numpoly.ndpoly): Polynomial based on input ``poly_like``. Examples: >>> numpoly.polynomial({(1,): 1}) polynomial(q) >>> x, y = numpoly.symbols("x y") >>> x**2 + x*y + 2 polynomial(x**2+x*y+2) >>> -3*x + x**2 + y polynomial(-3*x+x**2+y) >>> numpoly.polynomial([x*y, x, y]) polynomial([x*y, x, y]) >>> numpoly.polynomial([1, 2, 3]) polynomial([1, 2, 3]) >>> import sympy >>> x_, y_ = sympy.symbols("x, y") >>> numpoly.polynomial(3*x_*y_ - 4 + x_**5) polynomial(x**5+3*x*y-4) """ if poly_like is None: poly = numpoly.ndpoly( exponents=[(0, )], shape=(), names=names, dtype=dtype, ) poly[";"] = 0 elif isinstance(poly_like, dict): poly = numpoly.ndpoly(exponents=[(0, )], shape=()) exponents, coefficients = zip(*list(poly_like.items())) poly = numpoly.ndpoly.from_attributes( exponents=exponents, coefficients=coefficients, names=names, dtype=dtype, ) elif isinstance(poly_like, numpoly.ndpoly): if names is None: names = poly_like.names poly = numpoly.ndpoly.from_attributes( exponents=poly_like.exponents, coefficients=poly_like.coefficients, names=names, dtype=dtype, ) # assume polynomial converted to structured array elif isinstance(poly_like, numpy.ndarray) and poly_like.dtype.names: keys = numpy.asarray(poly_like.dtype.names, dtype="U") exponents = keys.flatten().view( numpy.uint32) - numpoly.ndpoly.KEY_OFFSET exponents = exponents.reshape(len(keys), -1) coefficients = [poly_like[key] for key in poly_like.dtype.names] poly = numpoly.ndpoly.from_attributes( exponents=exponents, coefficients=coefficients, names=names, ) elif isinstance(poly_like, (int, float, numpy.ndarray, numpy.generic)): poly = numpoly.ndpoly.from_attributes( exponents=[(0, )], coefficients=numpy.array([poly_like]), names=names, dtype=dtype, ) # handler for sympy objects elif hasattr(poly_like, "as_poly"): poly_like = poly_like.as_poly() exponents = poly_like.monoms() coefficients = [ int(coeff) if coeff.is_integer else float(coeff) for coeff in poly_like.coeffs() ] names = [str(elem) for elem in poly_like.gens] poly = numpoly.ndpoly.from_attributes( exponents=exponents, coefficients=coefficients, names=names, ) else: poly = compose_polynomial_array( arrays=poly_like, dtype=dtype, ) return poly
def floor_divide(x1, x2, out=None, where=True, **kwargs): """ Return the largest integer smaller or equal to the division of the inputs. It is equivalent to the Python ``//`` operator and pairs with the Python ``%`` (`remainder`), function so that ``a = a % b + b * (a // b)`` up to roundoff. Args: x1 (numpoly.ndpoly): Numerator. x2 (numpoly.ndpoly): Denominator. If ``x1.shape != x2.shape``, they must be broadcastable to a common shape (which becomes the shape of the output). out (Optional[numpy.ndarray]): A location into which the result is stored. If provided, it must have a shape that the inputs broadcast to. If not provided or `None`, a freshly-allocated array is returned. A tuple (possible only as a keyword argument) must have length equal to the number of outputs. where (Optional[numpy.ndarray]): This condition is broadcast over the input. At locations where the condition is True, the `out` array will be set to the ufunc result. Elsewhere, the `out` array will retain its original value. Note that if an uninitialized `out` array is created via the default ``out=None``, locations within it where the condition is False will remain uninitialized. kwargs: Keyword args passed to numpy.ufunc. Returns: (numpoly.ndpoly): This is a scalar if both `x1` and `x2` are scalars. Examples: >>> xyz = [1, 2, 4]*numpoly.symbols("x y z") >>> numpoly.floor_divide(xyz, 2.) polynomial([0.0, y, 2.0*z]) >>> numpoly.floor_divide(xyz, [1, 2, 4]) polynomial([x, y, z]) >>> numpoly.floor_divide([1, 2, 4], xyz) Traceback (most recent call last): ... ValueError: only constant polynomials can be converted to array. """ x1, x2 = numpoly.align_polynomials(x1, x2) x2 = x2.tonumpy() no_output = out is None if no_output: out = numpoly.ndpoly( exponents=x1.exponents, shape=x1.shape, names=x1.indeterminants, dtype=numpy.common_type(x1, numpy.array(1.)), ) for key in x1.keys: numpy.floor_divide(x1[key], x2, out=out[key], where=where, **kwargs) if no_output: out = numpoly.clean_attributes(out) return out
def polynomial_from_attributes( exponents, coefficients, names, dtype=None, clean=True, ): """ Construct polynomial from polynomial attributes. Args: exponents (numpy.ndarray): The exponents in an integer array with shape ``(N, D)``, where ``N`` is the number of terms in the polynomial sum and ``D`` is the number of dimensions. coefficients (Iterable[numpy.ndarray]): The polynomial coefficients. Must correspond to `exponents` by having the same length ``N``. names (Union[Sequence[str], numpoly.ndpoly]): The indeterminant names, either as string names or as simple polynomials. Must correspond to the exponents by having length ``D``. dtype (Optional[numpy.dtype]): The data type of the polynomial. If omitted, extract from `coefficients`. clean (bool): Clean up attributes, removing redundant indeterminant names and exponents. Returns: (numpoly.ndpoly): Polynomial array with attributes determined by the input. Examples: >>> numpoly.ndpoly.from_attributes( ... exponents=[(0,), (1,)], ... coefficients=[[1, 0], [0, 1]], ... names=("x",), ... ) polynomial([1, x]) >>> numpoly.ndpoly.from_attributes( ... exponents=[(0, 0, 0), (1, 1, 2)], ... coefficients=[4, -1], ... names=("x", "y", "z"), ... ) polynomial(4-x*y*z**2) >>> numpoly.ndpoly.from_attributes( ... exponents=[(0,)], ... coefficients=[0], ... ) polynomial(0) """ if clean: exponents, coefficients, names = clean_.postprocess_attributes( exponents, coefficients, names) if coefficients: dtype = coefficients[0].dtype if dtype is None else dtype shape = coefficients[0].shape else: dtype = dtype if dtype else int shape = () poly = numpoly.ndpoly( exponents=exponents, shape=shape, names=names, dtype=dtype, ) for exponent, values in zip(poly.keys, coefficients): poly[exponent] = values return poly
def polynomial_from_attributes( exponents: numpy.typing.ArrayLike, coefficients: Sequence[numpy.typing.ArrayLike], names: Union[None, str, Tuple[str, ...], ndpoly] = None, dtype: Optional[numpy.typing.DTypeLike] = None, allocation: Optional[int] = None, retain_coefficients: Optional[bool] = None, retain_names: Optional[bool] = None, ) -> ndpoly: """ Construct polynomial from polynomial attributes. Args: exponents: The exponents in an integer array with shape ``(N, D)``, where ``N`` is the number of terms in the polynomial sum and ``D`` is the number of dimensions. coefficients: The polynomial coefficients. Must correspond to `exponents` by having the same length ``N``. names: The indeterminant names, either as string names or as simple polynomials. Must correspond to the exponents by having length ``D``. dtype: The data type of the polynomial. If omitted, extract from `coefficients`. allocation: The maximum number of polynomial exponents. If omitted, use length of exponents for allocation. retain_coefficients: Do not remove redundant coefficients. If omitted use global defaults. retain_names: Do not remove redundant names. If omitted use global defaults. Returns: Polynomial array with attributes determined by the input. Examples: >>> numpoly.ndpoly.from_attributes( ... exponents=[(0,), (1,)], ... coefficients=[[1, 0], [0, 1]], ... names="q4", ... ) polynomial([1, q4]) >>> numpoly.ndpoly.from_attributes( ... exponents=[(0, 0, 0), (1, 1, 2)], ... coefficients=[4, -1], ... names=("q2", "q4", "q10"), ... ) polynomial(-q2*q4*q10**2+4) >>> numpoly.ndpoly.from_attributes( ... exponents=[(0,)], ... coefficients=[0], ... ) polynomial(0) """ exponents, coefficients, names = clean.postprocess_attributes( exponents=exponents, coefficients=coefficients, names=names, retain_coefficients=retain_coefficients, retain_names=retain_names, ) if coefficients: dtype = coefficients[0].dtype if dtype is None else dtype shape = coefficients[0].shape else: dtype = dtype if dtype else int shape = () poly = numpoly.ndpoly( exponents=exponents, shape=shape, names=names, dtype=dtype, allocation=allocation, ) for key, values in zip(poly.keys, coefficients): poly.values[key] = values return poly
def floor_divide( x1: PolyLike, x2: PolyLike, out: Optional[ndpoly] = None, where: Optional[numpy.ndarray] = numpy.array(True), **kwargs: Any, ) -> ndpoly: """ Return the largest integer smaller or equal to the division of the inputs. It is equivalent to the Python ``//`` operator and pairs with the Python ``%`` (`remainder`), function so that ``a = a % b + b * (a // b)`` up to roundoff. Args: x1: Dividend. x2: Divisor. If ``x1.shape != x2.shape``, they must be broadcastable to a common shape (which becomes the shape of the output). out: A location into which the result is stored. If provided, it must have a shape that the inputs broadcast to. If not provided or `None`, a freshly-allocated array is returned. A tuple (possible only as a keyword argument) must have length equal to the number of outputs. where: This condition is broadcast over the input. At locations where the condition is True, the `out` array will be set to the ufunc result. Elsewhere, the `out` array will retain its original value. Note that if an uninitialized `out` array is created via the default ``out=None``, locations within it where the condition is False will remain uninitialized. kwargs: Keyword args passed to numpy.ufunc. Returns: This is a scalar if both `x1` and `x2` are scalars. Raises: ValueError: If `x2` contains indeterminants, numerical division is no longer possible and an error is raised instead. For polynomial division see ``numpoly.poly_divide``. Examples: >>> numpoly.floor_divide([1, 3, 5], 2) polynomial([0, 1, 2]) >>> poly = [1, 2, 4]*numpoly.variable(3) >>> poly polynomial([q0, 2*q1, 4*q2]) >>> numpoly.floor_divide(poly, 2.) polynomial([0.0, q1, 2.0*q2]) >>> numpoly.floor_divide(poly, [1, 2, 4]) polynomial([q0, q1, q2]) """ x1, x2 = numpoly.align_polynomials(x1, x2) if not x2.isconstant(): raise numpoly.FeatureNotSupported(DIVIDE_ERROR_MSG) x2 = x2.tonumpy() dtype = numpy.common_type(x1, x2) if x1.dtype == x2.dtype == "int64": dtype = "int64" no_output = out is None if out is None: out = numpoly.ndpoly( exponents=x1.exponents, shape=x1.shape, names=x1.indeterminants, dtype=dtype, ) assert isinstance(out, numpoly.ndpoly) for key in x1.keys: out.values[key] = 0 numpy.floor_divide(x1.values[key], x2, out=out.values[key], where=where, **kwargs) if no_output: out = numpoly.clean_attributes(out) return out
def polynomial( poly_like: PolyLike = 0, names: Union[None, str, Tuple[str, ...], ndpoly] = None, dtype: Optional[numpy.typing.DTypeLike] = None, allocation: Optional[int] = None, ) -> ndpoly: """ Attempt to cast an object into a polynomial array. Supports various casting options: ================== ======================================================= ``dict`` Keys are tuples that represent polynomial exponents, and values are numpy arrays that represents polynomial coefficients. ``numpoly.ndpoly`` Copy of the polynomial. ``numpy.ndarray`` Constant term polynomial. ``sympy.Poly`` Convert polynomial from ``sympy`` to ``numpoly``, if possible. ``Iterable`` Multivariate array construction. structured array Assumes that the input are raw polynomial core and can be used to construct a polynomial without changing the data. Used for developer convenience. ================== ======================================================= Args: poly_like: Input to be converted to a `numpoly.ndpoly` polynomial type. names: Name of the indeterminant variables. If possible to infer from ``poly_like``, this argument will be ignored. dtype: Data type used for the polynomial coefficients. allocation: The maximum number of polynomial exponents. If omitted, use length of exponents for allocation. Returns: Polynomial based on input ``poly_like``. Examples: >>> numpoly.polynomial({(1,): 1}) polynomial(q0) >>> q0, q1 = numpoly.variable(2) >>> q0**2+q0*q1+2 polynomial(q0*q1+q0**2+2) >>> -3*q0+q0**2+q1 polynomial(q0**2+q1-3*q0) >>> numpoly.polynomial([q0*q1, q0, q1]) polynomial([q0*q1, q0, q1]) >>> numpoly.polynomial([1, 2, 3]) polynomial([1, 2, 3]) >>> import sympy >>> q0_, q1_ = sympy.symbols("q0, q1") >>> numpoly.polynomial(3*q0_*q1_-4+q0_**5) polynomial(q0**5+3*q0*q1-4) """ if isinstance(poly_like, dict): poly = numpoly.ndpoly(exponents=[(0, )], shape=()) exponents, coefficients = zip(*list(poly_like.items())) poly = numpoly.ndpoly.from_attributes( exponents=exponents, coefficients=coefficients, names=names, dtype=dtype, allocation=allocation, ) elif isinstance(poly_like, numpoly.ndpoly): if names is None: names = poly_like.names poly = numpoly.ndpoly.from_attributes( exponents=poly_like.exponents, coefficients=poly_like.coefficients, names=names, dtype=dtype, allocation=allocation, ) # assume polynomial converted to structured array elif isinstance(poly_like, numpy.ndarray) and poly_like.dtype.names: keys = numpy.asarray(poly_like.dtype.names, dtype="U") keys = numpy.array([key for key in keys if not key.isdigit()]) keys = numpy.array(keys, dtype=f"U{numpy.max(numpy.char.str_len(keys))}") exponents = keys.view(numpy.uint32) - numpoly.ndpoly.KEY_OFFSET exponents = exponents.reshape(len(keys), -1) coefficients = [poly_like[key] for key in poly_like.dtype.names] poly = numpoly.ndpoly.from_attributes( exponents=exponents, coefficients=coefficients, names=names, allocation=allocation, ) elif isinstance(poly_like, (int, float, numpy.ndarray, numpy.generic)): poly = numpoly.ndpoly.from_attributes( exponents=[(0, )], coefficients=[numpy.asarray(poly_like)], names=names, dtype=dtype, allocation=allocation, ) # handler for sympy objects elif hasattr(poly_like, "as_poly"): poly_like = poly_like.as_poly() # type: ignore exponents = poly_like.monoms() # type: ignore coefficients = [ int(coeff) if coeff.is_integer else float(coeff) # type: ignore for coeff in poly_like.coeffs() # type: ignore ] names = [str(elem) for elem in poly_like.gens] # type: ignore poly = numpoly.ndpoly.from_attributes( exponents=exponents, coefficients=coefficients, names=names, allocation=allocation, ) else: poly = compose_polynomial_array( arrays=poly_like, # type: ignore dtype=dtype, allocation=allocation, ) return poly
def monomial(start, stop=None, ordering="G", cross_truncation=1., names=None): """ Create an polynomial monomial expansion. Args: start (int, numpy.ndarray): The minimum polynomial to include. If int is provided, set as lowest total order. If array of int, set as lower order along each indeterminant. stop (int, numpy.ndarray): The maximum shape included. If omitted: ``stop <- start; start <- 0`` If int is provided, set as largest total order. If array of int, set as largest order along each indeterminant. ordering (str): The monomial ordering where the letters ``G``, ``I`` and ``R`` can be used to set grade, inverse and reverse to the ordering. For ``names=("x", "y"))`` we get for various values for ``ordering``: ======== ===================== ordering output ======== ===================== "" [1 y y**2 x x*y x**2] "G" [1 y x y**2 x*y x**2] "I" [x**2 x*y x y**2 y 1] "R" [1 x x**2 y x*y y**2] "GIR" [y**2 x*y x**2 y x 1] ======== ===================== cross_truncation (float): Use hyperbolic cross truncation scheme to reduce the number of terms in expansion. names (None, numpoly.ndpoly, str, Tuple[str, ...]) The indeterminants names used to create the monomials expansion. Returns: (numpoly.ndpoly): Monomial expansion. Examples: >>> numpoly.monomial(4) polynomial([1, q, q**2, q**3]) >>> numpoly.monomial(4, 5, ordering="GR", names=("x", "y")) polynomial([x**4, x**3*y, x**2*y**2, x*y**3, y**4]) >>> numpoly.monomial(2, [3, 4]) polynomial([q1**2, q0*q1, q0**2, q1**3]) >>> numpoly.monomial(0, 5, names=("x", "y"), cross_truncation=0.5) polynomial([1, y, x, y**2, x*y, x**2, y**3, x**3, y**4, x**4]) """ if stop is None: start, stop = numpy.array(0), start start = numpy.array(start, dtype=int) stop = numpy.array(stop, dtype=int) dimensions = 1 if names is None else len(names) dimensions = max(start.size, stop.size, dimensions) indices = bindex( start=start, stop=stop, dimensions=dimensions, ordering=ordering, cross_truncation=cross_truncation, ) poly = numpoly.ndpoly( exponents=indices, shape=(len(indices), ), names=names, ) for coeff, key in zip(numpy.eye(len(indices), dtype=int), poly.keys): poly[key] = coeff return poly
def multiply( x1: PolyLike, x2: PolyLike, out: Optional[ndpoly] = None, where: numpy.typing.ArrayLike = True, **kwargs: Any, ) -> ndpoly: """ Multiply arguments element-wise. Args: x1, x2: Input arrays to be multiplied. If ``x1.shape != x2.shape``, they must be broadcastable to a common shape (which becomes the shape of the output). out: A location into which the result is stored. If provided, it must have a shape that the inputs broadcast to. If not provided or `None`, a freshly-allocated array is returned. A tuple (possible only as a keyword argument) must have length equal to the number of outputs. where: This condition is broadcast over the input. At locations where the condition is True, the `out` array will be set to the ufunc result. Elsewhere, the `out` array will retain its original value. Note that if an uninitialized `out` array is created via the default ``out=None``, locations within it where the condition is False will remain uninitialized. kwargs: Keyword args passed to numpy.ufunc. Returns: The product of `x1` and `x2`, element-wise. This is a scalar if both `x1` and `x2` are scalars. Examples: >>> poly = numpy.arange(9.0).reshape((3, 3)) >>> q0q1q2 = numpoly.variable(3) >>> numpoly.multiply(poly, q0q1q2) polynomial([[0.0, q1, 2.0*q2], [3.0*q0, 4.0*q1, 5.0*q2], [6.0*q0, 7.0*q1, 8.0*q2]]) """ x1, x2 = numpoly.align_indeterminants(x1, x2) dtype = numpy.find_common_type([x1.dtype, x2.dtype], []) shape = numpy.broadcast_shapes(x1.shape, x2.shape) where = numpy.asarray(where) exponents = numpy.unique(numpy.tile(x1.exponents, (len(x2.exponents), 1)) + numpy.repeat(x2.exponents, len(x1.exponents), 0), axis=0) out_ = numpoly.ndpoly( exponents=exponents, shape=shape, names=x1.indeterminants, dtype=dtype, ) if out is None else out seen = set() for expon1, coeff1 in zip(x1.exponents, x1.coefficients): for expon2, coeff2 in zip(x2.exponents, x2.coefficients): key = (expon1 + expon2 + x1.KEY_OFFSET).ravel() key = key.view(f"U{len(expon1)}").item() if key in seen: out_.values[key] += numpy.multiply(coeff1, coeff2, where=where, **kwargs) else: numpy.multiply(coeff1, coeff2, out=out_.values[key], where=where, **kwargs) seen.add(key) if out is None: out_ = numpoly.clean_attributes(out_) return out_
def monomial( start: numpy.typing.ArrayLike, stop: Optional[numpy.typing.ArrayLike] = None, dimensions: int = 1, cross_truncation: numpy.typing.ArrayLike = 1., graded: bool = False, reverse: bool = False, allocation: Optional[int] = None, ) -> ndpoly: """ Create an polynomial monomial expansion. Args: start: The minimum polynomial to include. If int is provided, set as lowest total order. If array of int, set as lower order along each indeterminant. stop: The maximum shape included. If omitted: ``stop <- start; start <- 0`` If int is provided, set as largest total order. If array of int, set as largest order along each indeterminant. dimensions: The indeterminants names used to create the monomials expansion. If int provided, set the number of names to be used. cross_truncation: The hyperbolic cross truncation scheme to reduce the number of terms in expansion. In other words, it sets the :math:`q` in the :math:`L_q`-norm. graded: Graded sorting, meaning the indices are always sorted by the index sum. E.g. ``q0**2*q1**2*q2**2`` has an exponent sum of 6, and will therefore be consider larger than both ``q0**3*q1*q2``, ``q0*q1**2*q2`` and ``q0*q1*q2**2``, which all have exponent sum of 5. reverse: Reverse lexicographical sorting meaning that ``q0*q1**3`` is considered bigger than ``q0**3*q1``, instead of the opposite. allocation: The maximum number of polynomial exponents. If omitted, use length of exponents for allocation. Returns: Monomial expansion. Examples: >>> numpoly.monomial(4) polynomial([1, q0, q0**2, q0**3]) >>> numpoly.monomial(start=4, stop=5, dimensions=2, ... graded=True, reverse=True) polynomial([q1**4, q0*q1**3, q0**2*q1**2, q0**3*q1, q0**4]) >>> numpoly.monomial(2, [3, 4], graded=True) polynomial([q0**2, q0*q1, q1**2, q1**3]) >>> numpoly.monomial( ... start=0, ... stop=4, ... dimensions=("q2", "q4"), ... cross_truncation=0.5, ... graded=True, ... reverse=True, ... ) polynomial([1, q4, q2, q4**2, q2**2, q4**3, q2**3]) """ if stop is None: start, stop = numpy.array(0), start start = numpy.array(start, dtype=int) stop = numpy.array(stop, dtype=int) if isinstance(dimensions, str): names, dimensions = (dimensions, ), 1 elif isinstance(dimensions, int): dimensions = max(start.size, stop.size, dimensions) names = numpoly.variable(dimensions).names elif dimensions is None: dimensions = max(start.size, stop.size) names = numpoly.variable(dimensions).names else: names, dimensions = dimensions, len(dimensions) indices = numpoly.glexindex( start=start, stop=stop, dimensions=dimensions, graded=graded, reverse=reverse, cross_truncation=cross_truncation, ) poly = numpoly.ndpoly( exponents=indices, shape=(len(indices), ), names=names, allocation=allocation, ) for coeff, key in zip(numpy.eye(len(indices), dtype=int), poly.keys): numpy.ndarray.__setitem__(poly, key, coeff) return poly
def apply_along_axis( func1d: Callable[[PolyLike], PolyLike], axis: int, arr: PolyLike, *args: Any, **kwargs: Any, ) -> ndpoly: """ Apply a function to 1-D slices along the given axis. Execute `func1d(a, *args)` where `func1d` operates on 1-D arrays and `a` is a 1-D slice of `arr` along `axis`. This is equivalent to (but faster than) the following use of `ndindex` and `s_`, which sets each of ``ii``, ``jj``, and ``kk`` to a tuple of indices:: Ni, Nk = a.shape[:axis], a.shape[axis+1:] for ii in ndindex(Ni): for kk in ndindex(Nk): f = func1d(arr[ii+s_[:,]+kk]) Nj = f.shape for jj in ndindex(Nj): out[ii+jj+kk] = f[jj] Equivalently, eliminating the inner loop, this can be expressed as:: Ni, Nk = a.shape[:axis], a.shape[axis+1:] for ii in ndindex(Ni): for kk in ndindex(Nk): out[ii+s_[...,]+kk] = func1d(arr[ii+s_[:,]+kk]) Args: func1d: This function should accept 1-D arrays. It is applied to 1-D slices of `arr` along the specified axis. axis: Axis along which `arr` is sliced. arr: Input array. args: Additional arguments to `func1d`. kwargs: Additional named arguments to `func1d`. Returns: The output array. The shape of `out` is identical to the shape of `arr`, except along the `axis` dimension. This axis is removed, and replaced with new dimensions equal to the shape of the return value of `func1d`. So if `func1d` returns a scalar `out` will have one fewer dimensions than `arr`. Examples: >>> q0, q1 = numpoly.variable(2) >>> b = numpoly.polynomial([[1, 2, 3*q0], ... [3, 6*q1, 6], ... [2, 7, 9]]) >>> numpoly.apply_along_axis(numpoly.mean, 0, b) polynomial([2.0, 2.0*q1+3.0, q0+5.0]) >>> numpoly.apply_along_axis(numpoly.mean, 1, b) polynomial([q0+1.0, 2.0*q1+3.0, 6.0]) """ collection: List[ndpoly] = list() @wraps(func1d) def wrapper_func(array): """Wrap func1d function.""" # Align indeterminants in case slicing changed them array = numpoly.polynomial(array, names=arr.indeterminants, allocation=arr.allocation) array, _ = numpoly.align.align_indeterminants(array, arr.indeterminants) # Evaluate function out = func1d(array, *args, **kwargs) # Restore indeterminants in case func1d changed them. out, _ = numpoly.align.align_indeterminants(out, arr.indeterminants) # Return dummy index integer value that will be replaced with # polynomials afterwards. ret_val = len(collection) * numpy.ones(out.shape, dtype=int) collection.append(out) return ret_val # Initiate wrapper arr = numpoly.aspolynomial(arr) out = numpy.apply_along_axis(wrapper_func, axis=axis, arr=arr.values) # align exponents polynomials = numpoly.align.align_exponents(*collection) dtype = numpoly.result_type(*polynomials) # Store results into new array ret_val = numpoly.ndpoly( exponents=polynomials[0].exponents, shape=out.shape, names=polynomials[0].indeterminants, dtype=dtype, ).values for idx, polynomial in enumerate(polynomials): ret_val[out == idx] = polynomial.values return numpoly.polynomial( ret_val, dtype=dtype, names=polynomials[0].indeterminants, allocation=polynomials[0].allocation, )
def true_divide( x1: PolyLike, x2: PolyLike, out: Optional[ndpoly] = None, where: numpy.typing.ArrayLike = True, **kwargs: Any, ) -> ndpoly: """ Return true division of the inputs, element-wise. Instead of the Python traditional 'floor division', this returns a true division. True division adjusts the output type to present the best answer, regardless of input types. Args: x1: Dividend array. x2: Divisor array. If ``x1.shape != x2.shape``, they must be broadcastable to a common shape (which becomes the shape of the output). out: A location into which the result is stored. If provided, it must have a shape that the inputs broadcast to. If not provided or `None`, a freshly-allocated array is returned. A tuple (possible only as a keyword argument) must have length equal to the number of outputs. where: This condition is broadcast over the input. At locations where the condition is True, the `out` array will be set to the ufunc result. Elsewhere, the `out` array will retain its original value. Note that if an uninitialized `out` array is created via the default ``out=None``, locations within it where the condition is False will remain uninitialized. kwargs: Keyword args passed to numpy.ufunc. Returns: This is a scalar if both `x1` and `x2` are scalars. Raises: numpoly.baseclass.FeatureNotSupported: If `x2` contains indeterminants, numerical division is no longer possible and an error is raised instead. For polynomial division see ``numpoly.poly_divide``. Examples: >>> q0q1q2 = numpoly.variable(3) >>> numpoly.true_divide(q0q1q2, 4) polynomial([0.25*q0, 0.25*q1, 0.25*q2]) >>> numpoly.true_divide(q0q1q2, [1, 2, 4]) polynomial([q0, 0.5*q1, 0.25*q2]) """ x1, x2 = numpoly.align_polynomials(x1, x2) if not x2.isconstant(): raise numpoly.FeatureNotSupported(DIVIDE_ERROR_MSG) x2 = x2.tonumpy() if out is None: out_ = numpoly.ndpoly( exponents=x1.exponents, shape=x1.shape, names=x1.indeterminants, dtype=numpy.common_type(x1, numpy.array(1.)), ) else: assert len(out) == 1 out_ = out[0] assert isinstance(out_, numpoly.ndpoly) for key in x1.keys: out_[key] = 0 numpy.true_divide(x1.values[key], x2, out=out_.values[key], where=numpy.asarray(where), **kwargs) if out is None: out_ = numpoly.clean_attributes(out_) return out_
def full_like( a: PolyLike, fill_value: numpy.typing.ArrayLike, dtype: Optional[numpy.typing.DTypeLike] = None, order: str = "K", subok: bool = True, shape: Optional[Sequence[int]] = None, ) -> ndpoly: """ Return a full array with the same shape and type as a given array. Args: a: The shape and data-type of `a` define these same attributes of the returned array. fill_value: Fill value. Must be broadcast compatible with shape. dtype: Overrides the data type of the result. order: Overrides the memory layout of the result. 'C' means C-order, 'F' means F-order, 'A' means 'F' if `a` is Fortran contiguous, 'C' otherwise. 'K' means match the layout of `a` as closely as possible. subok: If True, then the newly created array will use the sub-class type of 'a', otherwise it will be a base-class array. Defaults to True. shape: Overrides the shape of the result. If order='K' and the number of dimensions is unchanged, will try to keep order, otherwise, order='C' is implied. Returns: Array of `fill_value` with the same shape and type as `a`. Examples: >>> poly = numpoly.monomial(3) >>> poly polynomial([1, q0, q0**2]) >>> q0, q1 = numpoly.variable(2) >>> numpoly.full_like(poly, q1-1.) polynomial([q1-1, q1-1, q1-1]) """ del subok if not isinstance(a, numpy.ndarray): a = numpoly.polynomial(a) fill_value = numpoly.aspolynomial(fill_value) if shape is None: shape = a.shape if dtype is None: dtype = a.dtype if order in ("A", "K"): order = "F" if a.flags["F_CONTIGUOUS"] else "C" out = numpoly.ndpoly( exponents=fill_value.exponents, shape=tuple(shape), names=fill_value.indeterminants, dtype=dtype, order=order, ) for key in fill_value.keys: out.values[key] = fill_value.values[key] return out
def compose_polynomial_array( arrays: Sequence[PolyLike], dtype: Optional[numpy.typing.DTypeLike] = None, allocation: Optional[int] = None, ) -> ndpoly: """ Compose polynomial from array of arrays of polynomials. Backend for `numpoly.polynomial` when input is undetermined. Args: arrays: Input to be converted to a `numpoly.ndpoly` polynomial type. dtype: Data type used for the polynomial coefficients. allocation: The maximum number of polynomial exponents. If omitted, use length of exponents for allocation. Return: Polynomial based on input `arrays`. """ oarrays = numpy.array(arrays, dtype=object) shape = oarrays.shape if not oarrays.size: return numpoly.ndpoly(shape=shape, dtype=dtype) if len(oarrays.shape) > 1: return numpoly.concatenate([ numpoly.expand_dims(numpoly.aspolynomial(array, dtype=dtype), axis=0) for array in arrays ], axis=0) oarrays = oarrays.flatten() indices = numpy.array( [isinstance(array, numpoly.ndpoly) for array in oarrays]) oarrays[indices] = numpoly.align_indeterminants(*oarrays[indices]) names = oarrays[indices][0] if numpy.any(indices) else None oarrays = oarrays.tolist() dtypes: List[numpy.typing.DTypeLike] = [] keys: Set[Tuple[int, ...]] = {(0, )} for array in oarrays: if isinstance(array, numpoly.ndpoly): dtypes.append(array.dtype) keys = keys.union({ tuple(int(k) for k in key) for key in array.exponents.tolist() }) elif isinstance(array, (numpy.generic, numpy.ndarray)): dtypes.append(array.dtype) else: dtypes.append(type(array)) if dtype is None: dtype = numpy.find_common_type(dtypes, []) length = max(1, max([len(key) for key in keys])) collection = {} for idx, array in enumerate(oarrays): if isinstance(array, numpoly.ndpoly): for key, value in zip(array.exponents, array.coefficients): key = tuple(key) + (0, ) * (length - len(key)) if key not in collection: collection[key] = numpy.zeros(len(oarrays), dtype=dtype) collection[key][idx] = value else: key = (0, ) * length if key not in collection: collection[key] = numpy.zeros(len(oarrays), dtype=dtype) collection[key][idx] = array exponents = sorted(collection) coefficients = numpy.array([collection[key] for key in exponents]) coefficients = coefficients.reshape(-1, *shape) return numpoly.ndpoly.from_attributes( exponents=exponents, coefficients=list(coefficients), names=names, allocation=allocation, )
def multiply(x1, x2, out=None, where=True, **kwargs): """ Multiply arguments element-wise. Args: x1, x2 (numpoly.ndpoly): Input arrays to be multiplied. If ``x1.shape != x2.shape``, they must be broadcastable to a common shape (which becomes the shape of the output). out (Optional[numpy.ndarray]): A location into which the result is stored. If provided, it must have a shape that the inputs broadcast to. If not provided or `None`, a freshly-allocated array is returned. A tuple (possible only as a keyword argument) must have length equal to the number of outputs. where (Optional[numpy.ndarray]): This condition is broadcast over the input. At locations where the condition is True, the `out` array will be set to the ufunc result. Elsewhere, the `out` array will retain its original value. Note that if an uninitialized `out` array is created via the default ``out=None``, locations within it where the condition is False will remain uninitialized. kwargs: Keyword args passed to numpy.ufunc. Returns: (numpoly.ndpoly): The product of `x1` and `x2`, element-wise. This is a scalar if both `x1` and `x2` are scalars. Examples: >>> x1 = numpy.arange(9.0).reshape((3, 3)) >>> xyz = numpoly.symbols("x y z") >>> numpoly.multiply(x1, xyz) polynomial([[0.0, y, 2.0*z], [3.0*x, 4.0*y, 5.0*z], [6.0*x, 7.0*y, 8.0*z]]) """ x1, x2 = numpoly.align_polynomials(x1, x2) no_output = out is None if no_output: exponents = (numpy.tile(x1.exponents, (len(x2.exponents), 1)) + numpy.repeat(x2.exponents, len(x1.exponents), 0)) out = numpoly.ndpoly( exponents=numpy.unique(exponents, axis=0), shape=x1.shape, names=x1.indeterminants, dtype=x1.dtype, ) seen = set() for expon1, coeff1 in zip(x1.exponents, x1.coefficients): for expon2, coeff2 in zip(x2.exponents, x2.coefficients): key = (expon1 + expon2 + x1.KEY_OFFSET).flatten() key = key.view("U%d" % len(expon1)).item() if key in seen: out[key] += numpy.multiply(coeff1, coeff2, where=where, **kwargs) else: numpy.multiply(coeff1, coeff2, out=out[key], where=where, **kwargs) seen.add(key) if no_output: out = numpoly.clean_attributes(out) return out