Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add general trig functions via taylor series #371

Merged
merged 27 commits into from
Jan 11, 2021
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
17f8cdb
Added sin and cos series expansions
hugohadfield Oct 19, 2020
0bee59d
Added tan and hyperbolic functions too
hugohadfield Oct 20, 2020
4ac8df8
Add docstrings
hugohadfield Oct 21, 2020
3d37b01
JIT general_exp
hugohadfield Oct 21, 2020
c7f0cd0
Move general trig functions
hugohadfield Oct 21, 2020
a7b671d
Fix inverse and add trig tests
hugohadfield Oct 21, 2020
89a116f
Use alg.scalar for readability
hugohadfield Oct 22, 2020
8c4cc47
Bump the default max order
hugohadfield Oct 22, 2020
e6c4f15
Refactor tests for concision
hugohadfield Oct 22, 2020
de66532
Allow pow to be overloaded in numba
hugohadfield Oct 22, 2020
183d269
JIT several of the trig functions
hugohadfield Oct 22, 2020
5a94b58
Added the complexified scalars as a test too
hugohadfield Oct 22, 2020
fe122e2
Move taylor expansions to their own file
hugohadfield Oct 22, 2020
0d13b3b
Remove cmath as numpy works fine
hugohadfield Oct 22, 2020
a6bd6f7
Use previous pow in series to calculate next term
hugohadfield Oct 22, 2020
a6cbde1
Rename taylor expansion functions
hugohadfield Dec 27, 2020
b3db4d3
Add taylor_expansions to index_rst
hugohadfield Dec 27, 2020
efa563d
Move general_exp deprecation to the original expected place
hugohadfield Dec 27, 2020
519a07a
Added a negative power error
hugohadfield Dec 27, 2020
d346969
improved docstring
hugohadfield Dec 27, 2020
dde49e6
Add a stub rst file for the taylor_expansions
hugohadfield Dec 27, 2020
7dd1e79
Add missing type annotations
hugohadfield Dec 27, 2020
09053b8
Add JIT comments
hugohadfield Dec 27, 2020
e7b4111
Remove relative import
hugohadfield Dec 28, 2020
662a7be
Include improved docstrings
hugohadfield Dec 28, 2020
ea63218
Include in changelog, expand to negative values for trig testing
hugohadfield Dec 28, 2020
9f0ed31
Fix docstrings and release note
hugohadfield Dec 28, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 6 additions & 38 deletions clifford/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@

from ._version import __version__ # noqa: F401
from . import _numba_utils
from . import _settings

from ._settings import pretty, ugly, eps, print_precision # noqa: F401
import clifford.taylor_expansions as taylor_expansions

# For backwards-compatibility. New code should import directly from `clifford.operator`
from .operator import gp, op, ip # noqa: F401
Expand All @@ -108,6 +108,11 @@
NUMBA_PARALLEL = not bool(NUMBA_DISABLE_PARALLEL)


def general_exp(x, **kwargs):
warnings.warn("cf.general_exp is deprecated. Use `mv.exp()` or `np.exp(mv)` on multivectors, or `cf.taylor_expansions.exp(x)` on arbitrary objects", DeprecationWarning, stacklevel=2)
return taylor_expansions.exp(x, **kwargs)


def linear_operator_as_matrix(func, input_blades, output_blades):
"""
Return a matrix that performs the operation of the provided linear
Expand Down Expand Up @@ -232,43 +237,6 @@ def grade_obj_func(objin_val, gradeList, threshold):
return np.argmax(modal_value_count)


def general_exp(x, max_order=15):
eric-wieser marked this conversation as resolved.
Show resolved Hide resolved
"""
This implements the series expansion of e**mv where mv is a multivector
The parameter order is the maximum order of the taylor series to use
"""

result = 1.0
if max_order == 0:
return result

# scale by power of 2 so that its norm is < 1
max_val = int(np.max(np.abs(x.value)))
scale = 1
if max_val > 1:
max_val <<= 1
while max_val:
max_val >>= 1
scale <<= 1

scaled = x * (1.0 / scale)

# taylor approximation
tmp = 1.0 + 0.0*x
for i in range(1, max_order):
if np.any(np.abs(tmp.value) > _settings._eps):
tmp = tmp*scaled * (1.0 / i)
result += tmp
else:
break

# undo scaling
while scale > 1:
result *= result
scale >>= 1
return result


def grade_obj(objin, threshold=0.0000001):
'''
Returns the modal grade of a multivector
Expand Down
4 changes: 2 additions & 2 deletions clifford/_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -577,11 +577,11 @@ def _hitzer_inverse(self):
"""
Performs the inversion operation as described in the paper :cite:`Hitzer_Sangwine_2017`
"""
tot = np.sum(self.sig != 0)
tot = len(self.sig)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note this is needed to get the correct behaviour for the tan test, presumably this is the solution for degenerate metric algebras but it would be nice to see a proof of this

@_numba_utils.njit
def hitzer_inverse(operand):
if tot == 0:
numerator = operand.layout.scalar
numerator = 1 + 0*operand
eric-wieser marked this conversation as resolved.
Show resolved Hide resolved
elif tot == 1:
# Equation 4.3
mv_invol = operand.gradeInvol()
Expand Down
24 changes: 21 additions & 3 deletions clifford/_multivector.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import numpy as np

import clifford as cf
from . import general_exp
import clifford.taylor_expansions as taylor_expansions
from . import _settings
from ._layout_helpers import layout_short_name

Expand Down Expand Up @@ -99,7 +99,25 @@ def _newMV(self, newValue=None, *, dtype: np.dtype = None) -> 'MultiVector':
# binary

def exp(self) -> 'MultiVector':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May as well copy this annotation to all the methods below it

return general_exp(self)
return taylor_expansions.exp(self)

def cos(self):
return taylor_expansions.cos(self)

def sin(self):
return taylor_expansions.sin(self)

def tan(self):
return taylor_expansions.tan(self)

def sinh(self):
return taylor_expansions.sinh(self)

def cosh(self):
return taylor_expansions.cosh(self)

def tanh(self):
return taylor_expansions.tanh(self)

def vee(self, other) -> 'MultiVector':
r"""
Expand Down Expand Up @@ -301,7 +319,7 @@ def __rpow__(self, other) -> 'MultiVector':
# else.

# pow(x, y) == exp(y * log(x))
newMV = general_exp(math.log(other) * self)
newMV = taylor_expansions.exp(math.log(other) * self)

return newMV

Expand Down
16 changes: 16 additions & 0 deletions clifford/numba/_multivector.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,22 @@ def impl(a, b):
return impl


@numba.extending.overload(operator.pow)
def ga_pow(a, b):
if isinstance(a, MultiVectorType) and isinstance(b, types.Integer):
gmt_func = a.layout_type.obj.gmt_func
def impl(a, b):
if b < 0:
raise NotImplementedError('Negative powers are currently not implemented')
if b == 0:
eric-wieser marked this conversation as resolved.
Show resolved Hide resolved
return 1 + 0*a
op = a.value
for i in range(1, b):
op = gmt_func(op, a.value)
return a.layout.MultiVector(op)
return impl


@numba.extending.overload(operator.truediv)
def ga_truediv(a, b):
if isinstance(a, MultiVectorType) and isinstance(b, types.abstract.Number):
Expand Down
121 changes: 121 additions & 0 deletions clifford/taylor_expansions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""
This file implements various Taylor expansions for useful functions of multivectors.
For some algebra signatures there may exist closed forms of these functions which would likely be faster
and more accurate. Nonetheless having pre-written taylor expansions for the general case is useful.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
and more accurate. Nonetheless having pre-written taylor expansions for the general case is useful.
and more accurate. Nonetheless, having pre-written taylor expansions for the general case is useful.
.. note::
Many of these functions are also exposed as :class:`~clifford.MultiVector` methods,
such as :meth:`clifford.MultiVector.sin`. This means that ``mv.sin()`` or even ``np.sin(mv)`` can be used
as a convenient interface to functions in this module, without having to import it directly.
For example::
>>> from clifford.g3 import *
>>> import numpy as np
>>> np.sin(np.pi*e12/4)
# what does this give?

"""

eric-wieser marked this conversation as resolved.
Show resolved Hide resolved
import math
import numpy as np

from . import _numba_utils
from . import _settings


@_numba_utils.njit
def exp(x, max_order=15):
"""
This implements the series expansion of e**mv where mv is a multivector
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This implements the series expansion of e**mv where mv is a multivector
This implements the series expansion of :math:`\exp x` where :math:`x` is a multivector

The parameter order is the maximum order of the taylor series to use
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The parameter order is the maximum order of the taylor series to use
The parameter `max_order` is the maximum order of the taylor series to use

"""

result = 1.0 + 0.0*x
if max_order == 0:
return result

# scale by power of 2 so that its norm is < 1
max_val = int(np.max(np.abs(x.value)))
scale = 1
if max_val > 1:
max_val <<= 1
while max_val:
max_val >>= 1
scale <<= 1

scaled = x * (1.0 / scale)

# taylor approximation
tmp = 1.0 + 0.0*x
for i in range(1, max_order):
if np.any(np.abs(tmp.value) > _settings._eps):
tmp = tmp*scaled * (1.0 / i)
result = result + tmp
else:
break

# undo scaling
while scale > 1:
result = result*result
scale >>= 1
return result


@_numba_utils.njit
def sin(X, max_order=30):
"""
A taylor series expansion for sin
"""
op = +X
X2 = X*X
X2np1 = X
for n in range(1, max_order):
X2np1 = X2np1 * X2
op = op + ((-1) ** (n) / math.gamma(2 * n + 2)) * X2np1
return op


@_numba_utils.njit
def cos(X, max_order=30):
"""
A taylor series expansion for cos
"""
op = 1 + 0*X
X2 = X * X
X2n = 1 + 0*X
for n in range(1, max_order):
X2n = X2n*X2
op = op + ((-1) ** (n) / math.gamma(2 * n + 1)) * X2n
return op


def tan(X, max_order=30):
eric-wieser marked this conversation as resolved.
Show resolved Hide resolved
"""
The tan function as the ratio of sin and cos
Note. It would probably be better to implement this as its own taylor series.
"""
return sin(X, max_order) / cos(X, max_order)


@_numba_utils.njit
def sinh(X, max_order=30):
"""
A taylor series expansion for sinh
"""
op = +X
X2 = X * X
X2np1 = X
for n in range(1, max_order):
X2np1 = X2np1 * X2
op = op + (1 / math.gamma(2 * n + 2)) * X2np1
return op


@_numba_utils.njit
def cosh(X, max_order=30):
"""
A taylor series expansion for cosh
"""
op = 1 + 0 * X
X2 = X * X
X2n = 1 + 0 * X
for n in range(1, max_order):
X2n = X2n * X2
op = op + (1 / math.gamma(2 * n + 1)) * X2n
return op


def tanh(X, max_order=30):
"""
The tanh function as the ratio of sinh and cosh
Note. It would probably be better to implement this as its own taylor series.
"""
return sinh(X, max_order) / cosh(X, max_order)
86 changes: 86 additions & 0 deletions clifford/test/test_trig_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@

from .. import Cl
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relative imports in tests are usually a bad idea - tests shouldn't assume they are part of the package.

Suggested change
from .. import Cl
from clifford import Cl

import numpy as np
import pytest


class TestScalarProperties:
eric-wieser marked this conversation as resolved.
Show resolved Hide resolved
@pytest.fixture()
def element(self):
alg, blades = Cl(0, 0, 0)
return alg.scalar

@pytest.mark.parametrize('np_func', [np.sin,
np.cos,
np.tan,
np.sinh,
np.cosh,
np.tanh])
def test_trig(self, element, np_func):
for x in np.linspace(0, 2*np.pi, 100):
assert abs(np_func(x*element).value[0] - np_func(x)) < 1E-10


class TestDualNumberProperties:
@pytest.fixture()
def element(self):
alg, blades = Cl(0, 0, 1)
return alg.scalar, blades['e1']

@pytest.mark.parametrize('func, deriv_func', [(np.sin, np.cos),
(np.cos, lambda x: -np.sin(x)),
(np.tan, lambda x: (1/np.cos(x)**2)),
(np.sinh, np.cosh),
(np.cosh, np.sinh),
(np.tanh, lambda x: (1 - np.tanh(x)**2))])
def test_derivatives(self, element, func, deriv_func):
for x in np.linspace(0, 2*np.pi, 10):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do these fare outside [0, 2*pi]? I'd be inclined to expand this to at least include some negative values,

Suggested change
for x in np.linspace(0, 2*np.pi, 10):
for x in np.linspace(-2*np.pi, 2*np.pi, 13):

result = func(x * element[0] + element[1])
assert abs(result.value[0] - func(x)) < 1E-10
assert abs(result.value[1] - deriv_func(x)) < 1E-10


class TestComplexNumberProperties:
@pytest.fixture()
def Cl010element(self):
alg, blades = Cl(0, 1, 0)
return alg.scalar, blades['e1']

@pytest.fixture()
def Cl000element(self):
alg, blades = Cl(0, 0, 0)
return alg.MultiVector(0*1j + alg.scalar.value), alg.MultiVector(1j*alg.scalar.value)

@pytest.mark.parametrize('np_func', [np.sin,
np.cos,
np.tan,
np.sinh,
np.cosh,
np.tanh])
def test_trig_Cl010(self, Cl010element, np_func):
"""
This tests the a clifford algebra isomorphic to the complex numbers
"""
for x in np.linspace(0, 2 * np.pi, 10):
for y in np.linspace(0, 2 * np.pi, 10):
complex_mv = x * Cl010element[0] + y * Cl010element[1]
complex_value = x + 1j * y
result = np_func(complex_mv)
assert abs(result.value[0] + 1j * result.value[1] - np_func(complex_value)) < 1E-10

@pytest.mark.parametrize('np_func', [np.sin,
np.cos,
np.tan,
np.sinh,
np.cosh,
np.tanh])
def test_trig_CxCl000(self, Cl000element, np_func):
"""
This tests the complexified clifford algebra of only the scalars
"""
for x in np.linspace(0, 2 * np.pi, 10):
for y in np.linspace(0, 2 * np.pi, 10):
complex_mv = x * Cl000element[0] + y * Cl000element[1]
complex_value = x + 1j * y
result = np_func(complex_mv)
assert abs(result.value[0] - np_func(complex_value)) < 1E-10
1 change: 1 addition & 0 deletions docs/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ API
tools
operator
transformations
taylor_expansions
eric-wieser marked this conversation as resolved.
Show resolved Hide resolved