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

Cleanup and write tests for clifford.cga #183

Merged
merged 3 commits into from
Oct 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
79 changes: 42 additions & 37 deletions clifford/cga.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
'''

from functools import reduce
from typing import overload
from . import conformalize, op, gp, MultiVector, Cl
from numpy import zeros, e, log
from numpy.random import rand
Expand All @@ -76,7 +77,17 @@ class CGAThing(object):

maps versor product to `__call__`.
'''
def __call__(self, other):
def __init__(self, cga: 'CGA') -> None:
self.cga = cga
self.layout = cga.layout

@overload # noqa: F811
def __call__(self, other: MultiVector) -> MultiVector: pass

@overload # noqa: F811
def __call__(self, other: 'CGAThing') -> 'CGAThing': pass

def __call__(self, other): # noqa: F811
if isinstance(other, MultiVector):
if other.grades() == {1}:
null = self.cga.null_vector(other)
Expand All @@ -86,7 +97,7 @@ def __call__(self, other):
klass = other.__class__
return klass(self.cga, self.mv*other.mv*~self.mv)

def inverted(self):
def inverted(self) -> MultiVector:
'''
inverted version of this thing.

Expand All @@ -96,7 +107,7 @@ def inverted(self):
'''
return self.cga.ep * self.mv * self.cga.ep

def involuted(self):
def involuted(self) -> MultiVector:
'''
inverted version of this thing.

Expand Down Expand Up @@ -137,10 +148,8 @@ class Flat(CGAThing):
>>> cga.flat(cga.flat().mv) # from existing multivector
'''
# could inherent some generic CGAObject class
def __init__(self, cga, *args):

self.cga = cga
self.layout = cga.layout # note: self.layout is the cga layout
def __init__(self, cga, *args) -> None:
super().__init__(cga)
self.einf = self.cga.einf # we use this alot

if len(args) == 0:
Expand Down Expand Up @@ -170,7 +179,7 @@ def __init__(self, cga, *args):

self.mv = self.mv.normal()

def __repr__(self):
def __repr__(self) -> str:
return '%i-Flat' % self.dim


Expand Down Expand Up @@ -203,10 +212,8 @@ class Round(CGAThing):
>>> cga.round(cga.flat().mv) # from existing multivector
'''
# could inherent some generic CGAObject class
def __init__(self, cga, *args):
# me
self.cga = cga
self.layout = cga.layout # note: self.layout is the cga layout
def __init__(self, cga, *args) -> None:
super().__init__(cga)

if len(args) == 0:
# generate random highest dimension round
Expand Down Expand Up @@ -248,7 +255,7 @@ def from_center_radius(self, center, radius):
self.mv = (center - .5*radius**2*self.cga.einf).normal().dual()
return self

def __repr__(self):
def __repr__(self) -> str:
names = {4: 'Sphere', 3: 'Circle', 2: 'Point Pair', 1: 'Point'}
if self.dim <= 4:
return names[self.dim + 2]
Expand Down Expand Up @@ -321,9 +328,8 @@ class Translation(CGAThing):
>>> T = cga.translation(cga.up(e1+e2)) # from null vector
>>> T = cga.translation(T.mv) # from existing translation rotor
'''
def __init__(self, cga, *args):
self.cga = cga
self.layout = cga.layout
def __init__(self, cga, *args) -> None:
super().__init__(cga)

if len(args) == 0:
# generate generator!
Expand All @@ -344,7 +350,7 @@ def __init__(self, cga, *args):

self.mv = mv

def __repr__(self):
def __repr__(self) -> str:
return 'Translation'


Expand All @@ -363,9 +369,8 @@ class Dilation(CGAThing):
>>> D = cga.dilation() # from none
>>> D = cga.dilation(.4) # from number
'''
def __init__(self, cga, *args):
self.cga = cga
self.layout = cga.layout
def __init__(self, cga, *args) -> None:
super().__init__(cga)

if len(args) == 0:
# generate a dilation
Expand All @@ -390,7 +395,7 @@ def __init__(self, cga, *args):

self.mv = mv

def __repr__(self):
def __repr__(self) -> str:
return 'Dilation'


Expand All @@ -415,9 +420,8 @@ class Rotation(CGAThing):
>>> R = cga.rotation(e12+e23) # from bivector
>>> R = cga.rotation(R.mv) # from bivector
'''
def __init__(self, cga, *args):
self.cga = cga
self.layout = cga.layout
def __init__(self, cga, *args) -> None:
super().__init__(cga)

if len(args) == 0:
# generate a rotation
Expand Down Expand Up @@ -449,7 +453,7 @@ def __init__(self, cga, *args):
# more than 1 arg
raise ValueError('bad input')

def __repr__(self):
def __repr__(self) -> str:
return 'Rotation'


Expand Down Expand Up @@ -481,10 +485,11 @@ class Transversion(Translation):
>>> K = cga.transversion(cga.up(e1+e2)) # from null vector
>>> K = cga.transversion(T.mv) # from existing translation rotor
'''
def __init__(self, cga, *args):
def __init__(self, cga, *args) -> None:
CGAThing.__init__(self, cga)
self.mv = Translation(cga, *args).inverted()

def __repr__(self):
def __repr__(self) -> str:
return 'Transversion'


Expand All @@ -511,21 +516,21 @@ class CGA(object):
>>> g3c = CGA(3)

'''
def __init__(self, layout_orig):
def __init__(self, layout_orig) -> None:
if isinstance(layout_orig, int):
layout_orig, blades = Cl(layout_orig)
self.layout_orig = layout_orig
self.layout, self.blades, stuff = conformalize(layout_orig)
self.__dict__.update(stuff)

# Objects
def base_vector(self):
def base_vector(self) -> MultiVector:
'''
random vector in the lower(original) space
'''
return self.I_base.project(self.layout.randomV())

def null_vector(self, x=None):
def null_vector(self, x=None) -> MultiVector:
'''
generates random null vector if x is None, or
returns a null vector from base vector x, if x^self.I_base ==0
Expand All @@ -540,45 +545,45 @@ def null_vector(self, x=None):
return self.up(x)
return x

def round(self, *args):
def round(self, *args) -> Round:
'''
see :class:`Round`
'''
return Round(self, *args)

def flat(self, *args):
def flat(self, *args) -> Flat:
'''
see :class:`Flat`
'''
return Flat(self, *args)

# Operators
def translation(self, *args):
def translation(self, *args) -> Translation:
'''
see :class:`Translation`
'''
return Translation(self, *args)

def transversion(self, *args):
def transversion(self, *args) -> Transversion:
'''
see :class:`Transversion`
'''
return Transversion(self, *args)

def dilation(self, *args):
def dilation(self, *args) -> Dilation:
'''
see :class:`Dilation`
'''
return Dilation(self, *args)

def rotation(self, *args):
def rotation(self, *args) -> Rotation:
'''
see :class:`Rotation`
'''
return Rotation(self, *args)

# methods
def straight_up(self, x):
def straight_up(self, x) -> MultiVector:
'''
place a vector from layout_orig into this CGA, without up()
'''
Expand Down
140 changes: 140 additions & 0 deletions clifford/test/test_cga.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import math

import pytest
import numpy.testing as npt

from clifford.cga import CGA


@pytest.fixture(scope='module')
def cga():
return CGA(3)


@pytest.mark.parametrize('method', [
CGA.round,
CGA.flat,
CGA.translation,
CGA.transversion,
CGA.dilation,
CGA.rotation,
])
def test_random_construction(cga, method):
method(cga)


@pytest.mark.parametrize('method', [
CGA.round,
pytest.param(CGA.flat, marks=pytest.mark.xfail(raises=AttributeError, reason='gh-180')),
CGA.translation,
CGA.transversion,
CGA.dilation,
CGA.rotation,
])
def test_repr(cga, method):
obj = method(cga)
repr(obj)


@pytest.mark.parametrize('method', [
CGA.round,
CGA.flat,
CGA.translation,
pytest.param(CGA.transversion, marks=pytest.mark.xfail(raises=AssertionError, reason='gh-182')),
pytest.param(CGA.dilation, marks=pytest.mark.xfail(raises=TypeError, reason='gh-181')),
CGA.rotation,
])
def test_copy_construction(cga, method):
# get a random one
obj = method(cga)

# and then copy
obj2 = method(cga, obj.mv)

assert obj.mv == obj2.mv


@pytest.mark.parametrize('method', [
CGA.round,
CGA.flat,
])
def test_dims_construction(cga, method):
obj = method(cga, 2)
if hasattr(obj, 'dim'):
assert obj.dim == 2

obj = method(cga, 1)
if hasattr(obj, 'dim'):
assert obj.dim == 1


@pytest.mark.parametrize('method', [
CGA.round,
pytest.param(CGA.flat, marks=pytest.mark.xfail(raises=AssertionError, reason='gh-100'))
])
def test_from_points_construction(cga, method):
blades = cga.layout.blades
e1 = blades['e1']
e2 = blades['e2']
e3 = blades['e3']
# can raise / lower without affecting the result
assert method(cga, e1, e2, e3).mv == method(cga, e1, e2, cga.up(e3)).mv


@pytest.mark.xfail(raises=AssertionError, reason='gh-184')
def test_round_from_center_radius_init(cga):
blades = cga.layout.blades
e1 = blades['e1']
e2 = blades['e2']

c = cga.round((3*e1+4*e2, 5))
npt.assert_almost_equal(c.center_down, 3*e1+4*e2)
npt.assert_almost_equal(c.radius, 5)


@pytest.mark.xfail(raises=AssertionError, reason='gh-184')
def test_round_from_center_radius_method(cga):
blades = cga.layout.blades
e1 = blades['e1']
e2 = blades['e2']

# TODO: this should be a static method, not a mutator
c = cga.round()
c.from_center_radius(30*e1+40*e2, 50)
npt.assert_almost_equal(c.center_down, 30.*e1+40.*e2)
npt.assert_almost_equal(c.radius, 50.)


class TestRotation:

def test_from_bivector(self, cga):
B = cga.layout.blades['e12']
cga.rotation(B*math.pi)

@pytest.mark.parametrize('theta', [
# produces a pure bivector rotor
pytest.param(math.pi/2, id='pi/2', marks=[
pytest.mark.xfail(raises=AssertionError, reason='gh-185')
]),
# produces a pure scalar rotor
pytest.param(math.pi, id='pi', marks=[
pytest.mark.xfail(raises=ValueError, reason='gh-185')
]),
])
def test_roundtrip(self, cga, theta):
""" Test that malicious rotations can still be reconstructed from their .mv """
B = cga.layout.blades['e12']
r = cga.rotation(B*theta)
r2 = cga.rotation(r.mv)
assert r.mv == r2.mv


def test_translation_from_vector(cga):
blades = cga.layout.blades
e1 = blades['e1']
e2 = blades['e2']
e3 = blades['e3']

t = cga.translation(e1 + e2)
assert t(e3) == cga.up(e1 + e2 + e3)
assert t(cga.up(e3)) == cga.up(e1 + e2 + e3)