Skip to content
Open
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
24 changes: 12 additions & 12 deletions RankPanda/CoreWrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ def GetCalculatedRanks(self):

def SetListOfSelectedCommandNumbers(self, commandNumberList):
if (self._song.currentMove is None):
return curList
return
allSelectedRanks = self._song.currentMove.GetSelectedRanks()
i = 0
while (i < len(allSelectedRanks)):
Expand Down Expand Up @@ -1120,7 +1120,7 @@ def GetTotalCounts(self):
# stepsPerMeasureList is of the form [(MeasureNumber, StepsPerCountValue)]
def GetLists(self):
countsPerMeasureItems = self._song.GetCountsPerMeasureIndex().items()
stepsPerMeasureItems = self._song.GetStepsPerCountIndex().items()
stepsPerCountItems = self._song.GetStepsPerCountIndex().items()
curList1 = []
i = 0
while (i < len(countsPerMeasureItems)):
Expand All @@ -1129,7 +1129,7 @@ def GetLists(self):
curList2 = []
i = 0
while (i < len(stepsPerCountItems)):
curList2.append(stepsPerCountIndex[i][0], stepsPerCountIndex[i][1])
curList2.append(stepsPerCountItems[i][0], stepsPerCountItems[i][1])
i = i + 1
return (curList1, curList2)

Expand All @@ -1140,29 +1140,29 @@ def GetListOfWayPoints(self):

# Change the basic info for a song. Pass in the same thing you would for making a new song. Should retain all the old move info, etc.
def EditSongInfo(self, newTitle, newNumberMeasures, newCountsPerMeasureList, newStepsPerCountList):
if (numberMeasures < 1):
if (newNumberMeasures < 1):
raise NameError("Number of measures can't be less than 1!")
if (len(CountsPerMeasureList) == 0):
if (len(newCountsPerMeasureList) == 0):
raise NameError("You must input the initial number of counts per measure!")
if (CountsPerMeasureList[0][0] != 1):
if (newCountsPerMeasureList[0][0] != 1):
raise NameError("You must input the initial number of counts per measure!")
self._song.SetTitle(newTitle)
self._song.SetNumberMeasures(newNumberMeasures)
self._song.ResetCountsPerMeasure(newCountsPerMeasureList[0][1])
self._song.ResetStepsPerCount()
i = 1
while (i < len(CountsPerMeasureList)):
if (CountsPerMeasureList[i][0] < 1):
while (i < len(newCountsPerMeasureList)):
if (newCountsPerMeasureList[i][0] < 1):
raise NameError("Measure number can't be less than 1!")
if (CountsPerMeasureList[i][1] < 0):
if (newCountsPerMeasureList[i][1] < 0):
raise NameError("Counts per Measure can't be less than 0!")
self._song.AddCountsPerMeasureChange(newCountsPerMeasureList[i][0], newCountsPerMeasureList[i][1])
i = i + 1
i = 0
while (i < len(StepsPerCountList)):
if (StepsPerCountList[i][0] < 1):
while (i < len(newStepsPerCountList)):
if (newStepsPerCountList[i][0] < 1):
raise NameError("Measure number can't be less than 1!")
if (StepsPerCountList[i][1] < 0):
if (newStepsPerCountList[i][1] < 0):
raise NameError("Steps per Count can't be less than 0!")
self._song.AddStepsPerCountChange(newStepsPerCountList[i][0], newStepsPerCountList[i][1])
i = i + 1
Expand Down
16 changes: 11 additions & 5 deletions RankPanda/CubicHermiteSpline.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ class SplineGenerator:
# This calculated what the slopes at the points should be for the splines.
# The method we use simply draws a straight line between the point before
# and the point after, and this becomes the slope of the point.
# It returns the slope at the middle point, which cannot be None (exception should be thrown)
# Note that the slopes are dx/dt and dy/dt, not dx/dy.
# Note that t always ranges between 0 and 1 from point to point.
@classmethod
def _GetSlope(cls, PointTriple):
p0 = PointTriple[0]
Expand All @@ -51,7 +53,7 @@ def _GetSlope(cls, PointTriple):
# The main splining method. Takes in a list of points and their slopes,
# and calculates the splines connecting them. Farily straightforward, once
# you know how it works, as described above.
# Note that is any slope is None, it'll automatically be calculated.
# Note that if any slope is None, it'll automatically be calculated.
# This should often be the case, except when doing something like
# intermediate DTP locations or something.
# A given spline is stored as a list of four elements:
Expand All @@ -66,7 +68,7 @@ def GetSplines(cls, pointList, oldSlopeList):
while (i < l):
if (oldSlopeList[i] is None):
if (i == 0):
slopeList.append(SplineGenerator._GetSlope([None, pointList[0], pointList[1]]))
slopeList.append(SplineGenerator._GetSlope([None, pointList[0], pointList[1]]))
elif (i == (l - 1)):
slopeList.append(SplineGenerator._GetSlope([pointList[i - 1], pointList[i], None]))
else:
Expand Down Expand Up @@ -97,6 +99,8 @@ def GetSplines(cls, pointList, oldSlopeList):
# from the midpoint to point 1. If the sum of these two lengths is close
# to the length of the first one, return the length. If not, recurse
# and add the lengths together.
# Doing this analytically isn't possible in the general case, I believe (requires finding the integral of the square root of a quartic function)
# Doing the integral numerically would probably be even more computationally intensive
@classmethod
def GetLength(cls, fnList, tol):
return SplineGenerator._GetLengthHelper(fnList, 0, 1, tol)
Expand Down Expand Up @@ -139,7 +143,8 @@ def EvalSlopeOfCubic(cls, t, fn):
# Increase the value of NUMBERPERSTEP to draw more points. Decrease to draw fewer.
# I return a list of lists - each inner list contains all the points to be
# drawn.

# TODO: make NUMBERPERSTEP a class variable so that we can change it
# TODO: make the number of points we return deterministic (it have a counter that counts up to total, instead of using t <= 1 as the loop guard)
@classmethod
def GetPoints(cls, splineList):
NUMBERPERSTEP = 8
Expand Down Expand Up @@ -171,12 +176,13 @@ def GetPoints(cls, splineList):
# The index is which spline part the point in question lies along.

# First, I get the lengths of each spline part. I then go through and find
# the total lenght along the splines needed, and find which spline part the
# the total length along the splines needed, and find which spline part the
# fraction will lie on.
# I then find the t value at which we need, and then just find the point
# and slope at tha t value.
# and slope at that value.
# Note: Not designed for repeated use in real time, because the finding of
# the t value recurses a recursive method until it's found.
# Only accurate to 1 decimal place (a tenth of a step)
@classmethod
def GetInformationAtLengthFraction(cls, splineList, lengthFraction):
lengths = SplineGenerator.GetLengths(splineList)
Expand Down
230 changes: 230 additions & 0 deletions RankPanda/CubicHermiteSpline_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
#!/usr/bin/env python
import unittest
import Point
import CubicHermiteSpline as CHS
import math


class TestCHS(unittest.TestCase):

# Expected return value for slopes when given all three input points is half the difference between
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Your comments are good, and helpful. It'd be better to make these docstrings so that they show up if someone calls help() on the function:

def testFoo():
"""Documentation
more documentation
"""
body...

# the value at 0 and the value at 2 for both x and y
def testGetSlopeAllThreePoints(self):
p0 = Point.Point(2, 3)
p1 = Point.Point(4, 7)
p2 = Point.Point(5, 1)

slopeSimple = CHS.SplineGenerator._GetSlope([p0, p1, p2])
self.assertEqual(slopeSimple.x, 1.5, "Failed to calculate slope for x when three points were specified.")
self.assertEqual(slopeSimple.y, -1, "Failed to calculate slope for y when three points were specified.")

# Here, we expect value at 2 minus the value at 1 (not halved this time because it's only half as far?
def testGetSlopeZerothNone(self):
p1 = Point.Point(4, 7)
p2 = Point.Point(5, 1)

slopeZerothNone = CHS.SplineGenerator._GetSlope([None, p1, p2])
self.assertEqual(slopeZerothNone.x, 1, "Failed to calculate slope for x when zeroth point was not specified.")
self.assertEqual(slopeZerothNone.y, -6, "Failed to calculate slope for y when zeroth point was not specified.")

# Here, we expect value at 1 minus the value at 2 (not halved this time because it's only half as far?
def testGetSlopeLastNone(self):
p0 = Point.Point(2, 3)
p1 = Point.Point(4, 7)

slopeLastNone = CHS.SplineGenerator._GetSlope([p0, p1, None])
self.assertEqual(slopeLastNone.x, 2, "Failed to calculate slope for x when zeroth point was not specified.")
self.assertEqual(slopeLastNone.y, 4, "Failed to calculate slope for y when zeroth point was not specified.")

# This should fail; we do not permit the middle Point to be None.
# Is this ok?
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Maybe we should file a bug for this? If something isn't behaving as you expect, that's a bug.

# Also: skipping doesn't seem to work. Wrong version of Python?
# @unittest.skip("Currently does not work: does not throw an exception but merely calculates ignoring middle point.")
# def testGetSlopeMiddleNone(self):
# p0 = Point.Point(2, 3)
# p2 = Point.Point(5, 1)

# TODO: Uncomment if test is actually correct.
# self.assertRaises(None, CHS.SplineGenerator._GetSlope, [p0, None, p2])
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I didn't know about assertRaises! It's definitely the right way to do it here. Another way to do this, which you might find more readable in other circumstances is:
try:
foo()
self.fail()
except ExceptionFoo:
pass


# Test the whole of the GetSplines method
# For this test, we simply pick a bunch of points, calculate the expected splines manually, and compare.
# This test is letting GetSplines calculate the slopes automatically.
def testGetSplinesNoSlope(self):
p0 = Point.Point(10,10)
p1 = Point.Point(15,15)
p2 = Point.Point(20,10)
p3 = Point.Point(30,10)
p4 = Point.Point(30,30)
p5 = Point.Point(10,20)

# Expected slopes:
# Yes I know this isn't a good unit test but it should stand until/unless we refactor GetSlopes out of CHS
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Perhaps add a TODO to remove after a refactor?

s0 = Point.Point(5, 5)
s1 = Point.Point(5, 0)
s2 = Point.Point(7.5, -2.5)
s3 = Point.Point(5, 10)
s4 = Point.Point(-10, 5)
s5 = Point.Point(-20, -10)

actualSplineList = CHS.SplineGenerator.GetSplines([p0,p1,p2,p3,p4,p5], [None,None,None,None,None,None])


# Not in a loop so that it's explicit what's going on:
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Could you give a comment as to what the math is doing? It's a little opaque.

xf0 = [p0.x, s0.x, (-3*p0.x + -2*s0.x + 3*p1.x + -1*s1.x), (2*p0.x + 1*s0.x + -2*p1.x + 1*s1.x)]
yf0 = [p0.y, s0.y, (-3*p0.y + -2*s0.y + 3*p1.y + -1*s1.y), (2*p0.y + 1*s0.y + -2*p1.y + 1*s1.y)]
xf1 = [p1.x, s1.x, (-3*p1.x + -2*s1.x + 3*p2.x + -1*s2.x), (2*p1.x + 1*s1.x + -2*p2.x + 1*s2.x)]
yf1 = [p1.y, s1.y, (-3*p1.y + -2*s1.y + 3*p2.y + -1*s2.y), (2*p1.y + 1*s1.y + -2*p2.y + 1*s2.y)]
xf2 = [p2.x, s2.x, (-3*p2.x + -2*s2.x + 3*p3.x + -1*s3.x), (2*p2.x + 1*s2.x + -2*p3.x + 1*s3.x)]
yf2 = [p2.y, s2.y, (-3*p2.y + -2*s2.y + 3*p3.y + -1*s3.y), (2*p2.y + 1*s2.y + -2*p3.y + 1*s3.y)]
xf3 = [p3.x, s3.x, (-3*p3.x + -2*s3.x + 3*p4.x + -1*s4.x), (2*p3.x + 1*s3.x + -2*p4.x + 1*s4.x)]
yf3 = [p3.y, s3.y, (-3*p3.y + -2*s3.y + 3*p4.y + -1*s4.y), (2*p3.y + 1*s3.y + -2*p4.y + 1*s4.y)]
xf4 = [p4.x, s4.x, (-3*p4.x + -2*s4.x + 3*p5.x + -1*s5.x), (2*p4.x + 1*s4.x + -2*p5.x + 1*s5.x)]
yf4 = [p4.y, s4.y, (-3*p4.y + -2*s4.y + 3*p5.y + -1*s5.y), (2*p4.y + 1*s4.y + -2*p5.y + 1*s5.y)]

expectedSplineList = [[xf0, yf0], [xf1, yf1], [xf2, yf2], [xf3, yf3], [xf4, yf4]]

self.assertEqual(len(actualSplineList), len(expectedSplineList), "Failed to calculate the correct number of spline functions")

for i in range (0, len(expectedSplineList)):
for j in range (0,2):
for k in range (0,4):
self.assertEqual(actualSplineList[i][j][k], expectedSplineList[i][j][k], "Error in calculating splines. Spline number " + str(i) + ", axis number " + str(j) + ", coefficient " + str(k) + ".")


# Test the whole of the GetSplines method
# For this test, we simply pick a bunch of points and slopes, and compare.
# This test is setting slopes explicitly (except one, to make sure it can still calculate it correctly.
def testGetSplinesInputSlope(self):
p0 = Point.Point(10,10)
p1 = Point.Point(15,15)
p2 = Point.Point(20,10)
p3 = Point.Point(30,10)
p4 = Point.Point(30,30)
p5 = Point.Point(10,20)

# Slopes (chosen by randomly stabbing the keyboard):
s0 = Point.Point(2, 4)
s1 = Point.Point(7, 7)
s2 = Point.Point(3.2, -3.6)
s3 = Point.Point(5, 10) # this one is what we expect to calculate
s4 = Point.Point(0, -23)
s5 = Point.Point(1, -1)

# Leave one None to make sure it still works:
actualSplineList = CHS.SplineGenerator.GetSplines([p0,p1,p2,p3,p4,p5], [s0,s1,s2,None,s4,s5])

# Not in a loop so that it's explicit what's going on:
xf0 = [p0.x, s0.x, (-3*p0.x + -2*s0.x + 3*p1.x + -1*s1.x), (2*p0.x + 1*s0.x + -2*p1.x + 1*s1.x)]
yf0 = [p0.y, s0.y, (-3*p0.y + -2*s0.y + 3*p1.y + -1*s1.y), (2*p0.y + 1*s0.y + -2*p1.y + 1*s1.y)]
xf1 = [p1.x, s1.x, (-3*p1.x + -2*s1.x + 3*p2.x + -1*s2.x), (2*p1.x + 1*s1.x + -2*p2.x + 1*s2.x)]
yf1 = [p1.y, s1.y, (-3*p1.y + -2*s1.y + 3*p2.y + -1*s2.y), (2*p1.y + 1*s1.y + -2*p2.y + 1*s2.y)]
xf2 = [p2.x, s2.x, (-3*p2.x + -2*s2.x + 3*p3.x + -1*s3.x), (2*p2.x + 1*s2.x + -2*p3.x + 1*s3.x)]
yf2 = [p2.y, s2.y, (-3*p2.y + -2*s2.y + 3*p3.y + -1*s3.y), (2*p2.y + 1*s2.y + -2*p3.y + 1*s3.y)]
xf3 = [p3.x, s3.x, (-3*p3.x + -2*s3.x + 3*p4.x + -1*s4.x), (2*p3.x + 1*s3.x + -2*p4.x + 1*s4.x)]
yf3 = [p3.y, s3.y, (-3*p3.y + -2*s3.y + 3*p4.y + -1*s4.y), (2*p3.y + 1*s3.y + -2*p4.y + 1*s4.y)]
xf4 = [p4.x, s4.x, (-3*p4.x + -2*s4.x + 3*p5.x + -1*s5.x), (2*p4.x + 1*s4.x + -2*p5.x + 1*s5.x)]
yf4 = [p4.y, s4.y, (-3*p4.y + -2*s4.y + 3*p5.y + -1*s5.y), (2*p4.y + 1*s4.y + -2*p5.y + 1*s5.y)]

expectedSplineList = [[xf0, yf0], [xf1, yf1], [xf2, yf2], [xf3, yf3], [xf4, yf4]]

self.assertEqual(len(actualSplineList), len(expectedSplineList), "Failed to calculate the correct number of spline functions")

for i in range (0, len(expectedSplineList)):
for j in range (0,2):
for k in range (0,4):
self.assertEqual(actualSplineList[i][j][k], expectedSplineList[i][j][k], "Error in calculating splines. Spline number " + str(i) + ", axis number " + str(j) + ", coefficient " + str(k) + ".")


# Testing GetLength
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This comment's a little superfluous :D

def testGetLength(self):

# Test some cases we can verify analytically:

# Straight line, vertical:
fnlist = [[5, 0, 0, 0],[2, 3, 0, 0]]
self.assertAlmostEqual(CHS.SplineGenerator.GetLength(fnlist, 0.0001), 3, 3, "GetLength incorrect for straight diagonal line case")

# Straight line, horizontal:
fnlist = [[5, 10, 0, 0],[2, 0, 0, 0]]
self.assertAlmostEqual(CHS.SplineGenerator.GetLength(fnlist, 0.0001), 10, 3, "GetLength incorrect for straight diagonal line case")

# A straight diagonal line:
fnlist = [[23, 3, 0, 0],[2, 4, 0, 0]];
self.assertAlmostEqual(CHS.SplineGenerator.GetLength(fnlist, 0.0001), 5, 3, "GetLength incorrect for straight diagonal line case")

# Should put in some more complicated cases but it's not trivial to find the length (without doing the computation of the method by hand.)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

TODO?


def testEvalCubic(self):
fn = [3, 6, 2, 8]
t = 0.23;
self.assertAlmostEqual(CHS.SplineGenerator.EvalCubic(t, fn), 8*t*t*t + 2*t*t + 6*t + 3, 6, "EvalCubic is incorrect")

def testEvalSlopeOfCubic(self):
fn = [3, 6, 2, 8]
t = 0.43;
self.assertAlmostEqual(CHS.SplineGenerator.EvalSlopeOfCubic(t, fn), 8*3*t*t + 2*2*t + 6, 6, "EvalSlopeOfCubic is incorrect")

# This method assumes that GetPoints's constant NUMBERPESTEP = 8.
# This should be changed to use a global constant.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Could you have this method link against the constant directly? That way, if we change it, the test won't break.

def testGetPoints(self):
splineList = [[[2, 3, 0, 0], [2, 4, 0, 0]], [[5, -5, 0, 0], [6, 12, 0, 0]]]

# This will generate 5 * 8 (+ 1?) = 40 or 41 points in the first list, and 13 * 8 + 1 = 104 or 105 points in the second list.

actualPoints = CHS.SplineGenerator.GetPoints(splineList)
self.assertTrue(math.fabs(len(actualPoints[0]) - 41) <= 1, "Wrong number of points in the first list")
self.assertTrue(math.fabs(len(actualPoints[1]) - 105) <= 1, "Wrong number of points in the second list")

# Test the points (all but the last)

for i in range(0, 40):
self.assertAlmostEqual(actualPoints[0][i].x, 2 + 3 * (i / (float(40))), 6)
self.assertAlmostEqual(actualPoints[0][i].y, 2 + 4 * (i / (float(40))), 6)

for i in range(0, 104):
self.assertAlmostEqual(actualPoints[1][i].x, 5 - 5 * (i / (float(104))), 6)
self.assertAlmostEqual(actualPoints[1][i].y, 6 + 12 * (i / (float(104))), 6)

# Use right triangles with integer lengths to make this simple...
# 3,4,5; 5, 12, 13; 8, 15, 17.
def testGetInformationAtLengthFraction(self):
splineList = [[[2, 3, 0, 0], [2, 4, 0, 0]], [[5, -5, 0, 0], [6, 12, 0, 0]], [[0, 8, 0, 0], [18, -15, 0, 0]]]

# Total length = 35 steps (5 + 13 + 17)

info0 = CHS.SplineGenerator.GetInformationAtLengthFraction(splineList, 0.1)
# This should be 3.5 steps into the 0th spline.

self.assertAlmostEqual(info0[0].x, 2 + 3 * (3.5 / 5.0), 2)
self.assertAlmostEqual(info0[0].y, 2 + 4 * (3.5 / 5.0), 2)
self.assertAlmostEqual(info0[1].x, 3, 2)
self.assertAlmostEqual(info0[1].y, 4, 2)
self.assertEqual(info0[2], 0)

info0 = CHS.SplineGenerator.GetInformationAtLengthFraction(splineList, 0.5)
# This should be 12.5 steps into the 1st spline (the middle one)

self.assertAlmostEqual(info0[0].x, 5 - 5 * (12.5 / 13.0), 1)
self.assertAlmostEqual(info0[0].y, 6 + 12 * (12.5 / 13.0), 1)
self.assertAlmostEqual(info0[1].x, -5, 1)
self.assertAlmostEqual(info0[1].y, 12, 1)
self.assertEqual(info0[2], 1)



if __name__ == '__main__':
unittest.main()











Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Please trim extra whitespace.



3 changes: 2 additions & 1 deletion RankPanda/Move.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import RankIDGen
import Rank
import pprint
import Commands


class Move(object):
Expand Down Expand Up @@ -233,7 +234,7 @@ def MergeWithPrior(self):
oldoldRank = priorprior.LookUpName(name)
if (oldoldRank is not None):
commandList = rank.GetCommantList()
commandList.append(Command.MarkTime(self._length, rank.GetEndLocation()))
commandList.append(Commands.MarkTime(self._length, rank.GetEndLocation()))
newRank.SetCommandList(commandList)
if (following is not None):
followingRank = following.LookUpName(name)
Expand Down
2 changes: 1 addition & 1 deletion RankPanda/Rank.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ def MergeCommands(self, firstCommandNumber):
if ((len(self._commandList)) > firstCommandNumber + 1):
return
firstCommand = self._commandList[firstCommandNumber]
secondCommand = self._commandList[secondCommandNumber]
secondCommand = self._commandList[firstCommandNumber + 1]
newCommand = firstCommand.MergeWithFollowing(secondCommand)
if (newCommand is not None):
self._commandList.pop(firstCommandNumber)
Expand Down
Loading