diff --git a/cli_application.py b/cli_application.py old mode 100644 new mode 100755 index 4d16962..cab353b --- a/cli_application.py +++ b/cli_application.py @@ -9,24 +9,32 @@ from user_interface.curve_parser import parse_curve_file from logger import get_logger +from data_objects.subdivision_generator import generate_curve_values class CliApplication(cmd.Cmd): - def __init__(self, application_path=None, application_name="generator", args=None): + def __init__(self, application_path=None, args="test.curve"): self.logger = get_logger(__name__) - self.curve_paths = os.path.join(application_path, "test_curves") + self.test_curves_path = os.path.join(application_path, "test_curves") self.arguments = args + self.logger.debug("Test Curves Path is: %s", self.test_curves_path) + self.logger.debug("Arguments are: %s", self.arguments) + self.parse_cmd_args(self.arguments) sys.exit(1) def parse_cmd_args(self, args): if args is not None: if args.generate_m_value: + self.logger.debug(args.generate_m_value) if os.path.exists(args.generate_m_value): - self.generate_m_value(args.generate_m_value) + if os.path.isdir(args.generate_m_value): + self.logger.error("Please input a filename not a directory name.") + else: + self.generate_m_value(args.generate_m_value) else: - curve_path = os.path.join(self.curve_paths, args.generate_m_value) + curve_path = os.path.join(self.test_curves_path, args.generate_m_value) if os.path.exists(curve_path): self.generate_m_value(curve_path) else: @@ -36,7 +44,7 @@ def parse_cmd_args(self, args): def generate_m_value(self, curve_file="test.curve"): knot_curve = parse_curve_file(curve_file) - + generate_curve_values(knot_curve.get_points_as_np_arrays()) pass @@ -53,7 +61,7 @@ def main(): arguments = argument_parser.parse_args() get_logger().debug("Subdivision-Generator Starting Up") - application = CliApplication(application_path, application_name, arguments) + application = CliApplication(application_path, arguments) if __name__ == "__main__": diff --git a/data_objects/generator.py b/data_objects/generator.py deleted file mode 100644 index e69de29..0000000 diff --git a/data_objects/stick_knot.py b/data_objects/stick_knot.py index fac6ae0..0addad3 100644 --- a/data_objects/stick_knot.py +++ b/data_objects/stick_knot.py @@ -3,6 +3,7 @@ @author Peter Zaffetti 2017 """ from point import Point +import numpy as np class StickKnot: @@ -13,3 +14,15 @@ def __init__(self, knot_points_arr=None): def get_points(self): return self.knot_points + + def get_points_as_np_arrays(self): + ''' + :return: the array of numpy arrays of point x, y, z values that make up the stick knot + ''' + points = list() + + for point in self.knot_points: + numpy_point = np.array([point.get_x(), point.get_y(), point.get_z()]) + points.append(numpy_point) + + return points diff --git a/data_objects/subdivision_generator.py b/data_objects/subdivision_generator.py new file mode 100644 index 0000000..1c2ebfb --- /dev/null +++ b/data_objects/subdivision_generator.py @@ -0,0 +1,364 @@ +import math +import numpy as np +from logger import get_logger + + +def closestDistanceBetweenLines(a0, a1, b0, b1, clampAll=False, clampA0=False, clampA1=False, clampB0=False, clampB1=False): + ''' Given two lines defined by numpy.array pairs (a0,a1,b0,b1) + Return distance, the two closest points, and their average + ''' + # If clampAll=True, set all clamps to True + if clampAll: + clampA0 = True + clampA1 = True + clampB0 = True + clampB1 = True + + # Calculate denomitator + A = a1 - a0 + B = b1 - b0 + + normA = np.linalg.norm(B) + if normA == 0: + print("HERE", b0, b1) + + _A = A / np.linalg.norm(A) + _B = B / np.linalg.norm(B) + + cross = np.cross(_A, _B); + denom = np.linalg.norm(cross) ** 2 + + # If denominator is 0, lines are parallel: Calculate distance with a projection + # and evaluate clamp edge cases + if (denom == 0): + d0 = np.dot(_A, (b0 - a0)) + d = np.linalg.norm(((d0 * _A) + a0) - b0) + # If clamping: the only time we'll get closest points will be when lines don't overlap at all. + # Find if segments overlap using dot products. + if clampA0 or clampA1 or clampB0 or clampB1: + d1 = np.dot(_A, (b1 - a0)) + + # Is segment B before A? + if d0 <= 0 >= d1: + if clampA0 == True and clampB1 == True: + if np.absolute(d0) < np.absolute(d1): + return b0, a0, np.linalg.norm(b0 - a0) + return b1, a0, np.linalg.norm(b1 - a0) + + # Is segment B after A? + elif d0 >= np.linalg.norm(A) <= d1: + if clampA1 == True and clampB0 == True: + if np.absolute(d0) < np.absolute(d1): + return b0, a1, np.linalg.norm(b0 - a1) + return b1, a1, np.linalg.norm(b1, a1) + + # If clamping is off, or segments overlapped, we have infinite results, just return position. + return None, None, d + + # Lines criss-cross: Calculate the dereminent and return points + t = (b0 - a0); + det0 = np.linalg.det([t, _B, cross]) + det1 = np.linalg.det([t, _A, cross]) + t0 = det0 / denom; + t1 = det1 / denom; + + pA = a0 + (_A * t0); + pB = b0 + (_B * t1); + + # Clamp results to line segments if needed + if clampA0 or clampA1 or clampB0 or clampB1: + if t0 < 0 and clampA0: + pA = a0 + elif t0 > np.linalg.norm(A) and clampA1: + pA = a1 + if t1 < 0 and clampB0: + pB = b0 + elif t1 > np.linalg.norm(B) and clampB1: + pB = b1 + + d = np.linalg.norm(pA - pB) + return pA, pB, d + + +''' +Calculate the distance of the line segment between the start and end points +''' +def distanceBetweenTwoPoints(startPoint, endPoint): + return np.linalg.norm(endPoint - startPoint) + + +''' +A function used to reduce a line segment between the startPoint and the endPoint +by the recduce amount. So if you have a line that is 1 distance long and you want to +move both of its end points in by .25 this function will return the two new end points +and the length of the new line will be .5. +''' +def reduceLineSegment(startPoint, endPoint, reduceAmount): + vector = [] + ''' + Parametrize the line segment so that you can subtract the distace. Since the line + is parametrized its distance will be from 0 to 1. To reduce it by the desired amount + we need to figure out the proportion of the line compared to the parameter and then + subtract that amount. + ''' + vector.append(startPoint[0] - endPoint[0]) # Calculate Vector X + vector.append(startPoint[1] - endPoint[1]) # Calculate Vector Y + vector.append(startPoint[2] - endPoint[2]) # Calculate Vector Z + dist = distanceBetweenTwoPoints(startPoint, endPoint) + + proportion = reduceAmount / dist + newX = endPoint[0] + proportion * vector[0] + newY = endPoint[1] + proportion * vector[1] + newZ = endPoint[2] + proportion * vector[2] + + newEndPoint = np.array([newX, newY, newZ]) + output = [] + newX = endPoint[0] + (1 - proportion) * vector[0] + newY = endPoint[1] + (1 - proportion) * vector[1] + newZ = endPoint[2] + (1 - proportion) * vector[2] + + newStartPoint = np.array([newX, newY, newZ]) + + output.append(newStartPoint) + output.append(newEndPoint) + return output + +''' +Generates the set of second iterated forward difference operator. This comes from Professor Peters' and +Ji Li's paper. In the paper it is define. It shows up as a triangle with a subscript 2 and and set P next to it +''' +def secondIteratedForwardDifferenceOperator(listOfControlPoints, index): + deltaSub2Set = [] + numCtrlPts = len(listOfControlPoints) + + # Need to wrap around and connect back to the first point + for i in range(0, numCtrlPts): # Add -1 to numCtrlPts to make it an open curve again + deltaSub2Ofi = listOfControlPoints[(i + 2) % numCtrlPts][index] - (2 * listOfControlPoints[(i + 1) % numCtrlPts][index]) + listOfControlPoints[i][index] + deltaSub2Set.append(deltaSub2Ofi) + return deltaSub2Set + +''' +This is the l1 - Norm for a set. The l1 norm is just the summation of the absolute value of all the +elements in the set. +''' +def l1Norm(setForNorming): + norm = 0 + i = 0 + + for element in setForNorming: + norm += abs(element) + i += 1 + + # print('Terms in L1 Norm: ', i) + + return norm + +''' +This generates the Omega one value from Professor Peters' and Ji Li's paper. Omega one is used in combination with the delta from the Denne-Sullivan paper to figure out m1 +''' +def omegaOne(listOfControlPoints): + deltaSetX = secondIteratedForwardDifferenceOperator(listOfControlPoints, 0) + deltaSetY = secondIteratedForwardDifferenceOperator(listOfControlPoints, 1) + deltaSetZ = secondIteratedForwardDifferenceOperator(listOfControlPoints, 2) + + l1NormX = l1Norm(deltaSetX) + l1NormY = l1Norm(deltaSetY) + l1NormZ = l1Norm(deltaSetZ) + + return max(l1NormX, l1NormY, l1NormZ) + + +''' +This function generates the m1 value from Professor Peters' and Ji Li's paper. M1 is compared against +m2 and m3 to determine the number of iterations, j, that need to be done in order for the stick knot +to properly associate with its correct bezier curve +''' +def generateM1(omegaOne, delta): + omegaOneSquared = np.multiply(omegaOne, omegaOne) + deltaSquared = delta * delta + intermediate = ((float(7) / float(16) * omegaOneSquared * (1 / deltaSquared)) - (float(1) / float(7))) + logResult = math.log(intermediate, 2) + return math.ceil(logResult) + +''' +Generates the hodograph delta sub 2 set from Professor Peters' and Ji Li's paper. It is defined in the paper +''' +def generateHodographDeltaTwoSet(listOfControlPoints, index): + hodographDelta2Set = [] + numControlPoints = len(listOfControlPoints) + + # Need to wrap around and connect back to the first point + for i in range(1, numControlPoints): # Add -1 to numControlPoints to make it an open curve again + hodographDeltaSub2Ofi = numControlPoints * (listOfControlPoints[(i + 2) % numControlPoints][index] - ( + 3 * listOfControlPoints[(i + 1) % numControlPoints][index]) + (3 * listOfControlPoints[i][index]) - + listOfControlPoints[i - 1][index]) + hodographDelta2Set.append(hodographDeltaSub2Ofi) + + return hodographDelta2Set + + +''' +Found the same way as Omega One but uses the hodograph sets instead +''' +def omegaTwo(listOfControlPoints): + hodographX = generateHodographDeltaTwoSet(listOfControlPoints, 0) + hodographY = generateHodographDeltaTwoSet(listOfControlPoints, 1) + hodographZ = generateHodographDeltaTwoSet(listOfControlPoints, 2) + + l1NormX = l1Norm(hodographX) + l1NormY = l1Norm(hodographY) + l1NormZ = l1Norm(hodographZ) + + return max(l1NormX, l1NormY, l1NormZ) + + +''' +The M2 value from Professor Peters' and Ji Li's paper. It is found using the hodograph set +''' + + +def generateM2(omegaTwo, lambdaVal, numCtrlPts): + j = 0 + + # This gets the next int higher to what would be an unreducible logarithm + while ((numCtrlPts * math.pow(2, 3 * j) + math.pow(2, 2 * j)) < math.pow(omegaTwo / lambdaVal, 2)): + j += 1 + return j + + +''' +The M3 value from Professor Peters' and Ji Li's paper. It is very similar to M2 except for the additional sin(pi/ 8) value +''' +def generateM3(omegaTwo, lambdaVal, numCtrlPts): + j = 0 + + # This gets the next int higher to what would be an unreducible logarithm + while ((numCtrlPts * math.pow(2, 3 * j) + math.pow(2, 2 * j)) < math.pow( + (omegaTwo / (math.sin(math.pi / 8) * lambdaVal)), 2)): + j += 1 + return j + + +def generate_curve_values(segment_array=[]): + ''' + Add the line segments of the control polygon to a list + ''' + logger = get_logger(generate_curve_values.__name__) + if len(segment_array) == 0: + logger.error("There were no curve points provided. Unable to generate the values.") + return + + minimums = [] + + ''' + For each line segment, compare it to all non-incident (non-adjacent) line segments. Add the distances + to the minimums list. + ''' + num_ctrl_point_segments = len(segment_array) + for i in range(0, num_ctrl_point_segments): + # print'i is: ' ,i + j = i + 2 + while j < num_ctrl_point_segments: + if i == 0 and (j + 1) % num_ctrl_point_segments == 0: + j += 1 + continue + + # print 'j is: ', j, ' and j + 1 mod 7 is', (j+1)% num_ctrl_point_segments + [a, b, dist] = closestDistanceBetweenLines(segment_array[i], segment_array[i + 1], segment_array[j], + segment_array[(j + 1) % num_ctrl_point_segments], clampAll=True) + # print('a is: ', a, ' b is: ', b, ' dist is: ', dist) + + # print(dist) + minimums.append(dist) + j += 1 + ''' + Of all the distances, take the minimum. This is the r1 value from the Denne-Sullivan paper + ''' + r1 = min(minimums) + logger.info('r1 is: %s' % r1) + + ''' + Epsilon is a constant that we have pre selected and can change + ''' + epsilon = 1.0 + logger.info("epsilon is: %s" % epsilon) + + ''' + r2 is also from the Denne-Sullivan paper + ''' + r2 = min(r1 / 2, epsilon / 2) + logger.info('r2 is: %s' % r2) + + ''' + For each line segment of the control polygon, remove r2 from each end. + ''' + adjusted_segments = [] + for i in range(0, num_ctrl_point_segments): + # print 'i is: ', i, 'and the Line Segment is ', segmentArray[i], segmentArray[(i+1)%num_ctrl_point_segments] + [segment1, segment2] = reduceLineSegment(segment_array[i], segment_array[(i + 1) % num_ctrl_point_segments], r2) + adjusted_segments.append(segment1) + adjusted_segments.append(segment2) + + # print 'The new segment is: ', segment1, segment2 + i += 1 + + # At this point the segments have been reduced along the vertex. This means that the number of points is 2 times the number of control points. This is because the segments no longer share any common points between themselves + num_adjusted_points = len(adjusted_segments) + + new_minimums = [] + + ''' + Then, for each reduced segment find the distance from that segment to all other reduced segments + ''' + + i = 0 + while i + 3 < num_adjusted_points: + j = i + 2 + while j + 1 < num_adjusted_points: + # print 'Comparing: ', adjusted_segments[i], adjusted_segments[i+1], 'to: ', adjusted_segments[j],adjusted_segments[j+1] + [a, b, dist] = closestDistanceBetweenLines(adjusted_segments[i], adjusted_segments[i + 1], + adjusted_segments[j], adjusted_segments[j + 1], clampAll=True) + new_minimums.append(dist) + # print dist + j += 2 + i += 2 + + ''' + Take r3 (also from Denne-Sullivan) to be the minimum distance of all distances between line segments + ''' + r3 = min(new_minimums) + logger.info('r3 is: %s' % r3) + + ''' + r4 is also from Denne-Sullivan paper + ''' + r4 = r3 / 6 + logger.info('r4 is: %s' % r4) + + ''' + Finally, calculate delta (from Denne-Sullivan) + ''' + delta = r4 / 3 + logger.info('delta is: %s' % delta) + + omega1 = omegaOne(segment_array) + logger.info('Omega One is: %s' % omega1) + + m1 = generateM1(omega1, delta) + logger.info('M1 is: %s' % m1) + + segment_lengths = [] + for i in range(0, num_ctrl_point_segments): + segment_lengths.append(distanceBetweenTwoPoints(segment_array[i], segment_array[(i + 1) % num_ctrl_point_segments])) + + lambda_val = min(segment_lengths) + logger.info('Lambda is: %s' % lambda_val) + + omega2 = omegaTwo(segment_array) + logger.info('Omega Two is: %s' % omega2) + + m2 = generateM2(omega2, lambda_val, num_ctrl_point_segments) + logger.info('M2 is: %s' % m2) + + m3 = generateM3(omega2, lambda_val, num_ctrl_point_segments) + logger.info('M3 is: %s' % m3) diff --git a/logger.py b/logger.py index 3d3bfaf..595c9aa 100644 --- a/logger.py +++ b/logger.py @@ -18,13 +18,24 @@ def get_logger(classname=None): for handler in root.handlers: root.removeHandler(handler) - logging.basicConfig(format='%(asctime)s: %(filename)s: %(funcName)s: %(levelname)s:\t %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p', filename="logs/generator_log.txt") + # logging.basicConfig(format='%(asctime)s: %(filename)s: %(funcName)s: %(levelname)s:\t %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p', filename="logs/generator_log.txt") if classname is None: classname = "Subdivision Generator" logger = logging.getLogger(classname) - logger.setLevel(logging.INFO) - logger.addHandler(logging.StreamHandler()) - return logging.getLogger(classname) + log_format = logging.Formatter("%(asctime)s: %(filename)s: %(funcName)s: %(levelname)s:\t %(message)s") + file_handler = logging.FileHandler("logs/generator_log.txt") + file_handler.setFormatter(log_format) + file_handler.setLevel(logging.DEBUG) + logger.addHandler(file_handler) + + console_handler = logging.StreamHandler() + console_handler.setFormatter(log_format) + console_handler.setLevel(logging.INFO) + logger.addHandler(console_handler) + + logger.setLevel(logging.DEBUG) + + return logger diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..bf16c2c --- /dev/null +++ b/setup.py @@ -0,0 +1,15 @@ +import sys +from cx_Freeze import setup, Executable + +# Dependencies are automatically detected, but it might need fine tuning. +build_exe_options = {"packages": ["os"], "excludes": ["tkinter"]} + +# GUI applications require a different base on Windows (the default is for a +# console application). +base = None + +setup(name="Subdivision-Generator", + version="0.1", + description="A program to create subdivisions for a given which will ", + options={"build_exe": build_exe_options}, + executables=[Executable("cli_application.py", base=base)]) \ No newline at end of file diff --git a/test_curves/Hulls_Trefoil_2017_09_03.curve b/test_curves/Hulls_Trefoil_2017_09_03.curve index 19d0e01..3288cbb 100644 --- a/test_curves/Hulls_Trefoil_2017_09_03.curve +++ b/test_curves/Hulls_Trefoil_2017_09_03.curve @@ -1,8 +1,8 @@ % Original Trefoil Control Points -0, 4831838208, 10737418240 --8053063680, -51002736640, -26843545600 % Test inline comment -21474836480, 42949672960, -10737418240 -5368709120, -32212254720, 31138512896 --32212254720, 16106127360, 10737418240 -21474836480, -32212254720, -32212254720 -0, 4831838208, 10737418240 \ No newline at end of file +0, 4.831838208, 10.737418240 +-8.053063680, -51.002736640, -26.843545600 +21.474836480, 42.949672960, -10.737418240 +5.368709120, -32.212254720, 31.138512896 +-32.212254720, 16.106127360, 10.737418240 +21.474836480, -32.212254720, -32.212254720 +%0, 4.831838208, 10.737418240 \ No newline at end of file diff --git a/test_curves/Hulls_Unknot_2017_09_03.curve b/test_curves/Hulls_Unknot_2017_09_03.curve index 774df69..6438146 100644 --- a/test_curves/Hulls_Unknot_2017_09_03.curve +++ b/test_curves/Hulls_Unknot_2017_09_03.curve @@ -1,8 +1,8 @@ % Original Unknot Control Points -0, 4831838208, 10737418240 --8053063680, -51002736640, -26843545600 -21474836480, 42949672960, -10737418240 --5368709120, -32212254720, 31138512896 --32212254720, 16106127360, 10737418240 -21474836480, -32212254720, -32212254720 -0, 4831838208, 10737418240 \ No newline at end of file +0, 4.831838208, 10.737418240 +-8.053063680, -51.002736640, -26.843545600 +21.474836480, 42.949672960, -10.737418240 +-5.368709120, -32.212254720, 31.138512896 +-32.212254720, 16.106127360, 10.737418240 +21.474836480, -32.212254720, -32.212254720 +%0, 4.831838208, 10.737418240 \ No newline at end of file diff --git a/user_interface/curve_parser.py b/user_interface/curve_parser.py index 4c0be08..29f1311 100644 --- a/user_interface/curve_parser.py +++ b/user_interface/curve_parser.py @@ -31,4 +31,12 @@ def parse_curve_file(filename="test.curve"): z_val = float(comma_split_point_values[2]) stick_knot_points.append(Point(x_val, y_val, z_val)) - return StickKnot(stick_knot_points) \ No newline at end of file + """ + PDZ- TODO: right now the generator can't exactly handle if there are two duplicate points in the curve. It will + die when it tries to compute the closest point. Temporary solution for now is to remove the last point if it is + the same as the first + """ + # if stick_knot_points[0] == stick_knot_points[-1]: + # stick_knot_points.remove(stick_knot_points[-1]) + + return StickKnot(stick_knot_points)