From f31957d2998fad37961c2de5b21aa47e83271b3b Mon Sep 17 00:00:00 2001 From: cedrusx Date: Wed, 16 Mar 2016 23:27:55 +0800 Subject: [PATCH] Encapsulate rules and constraints, #1 Hide the complexity of rule-specific operations. In order to make main.py conciser and make it easier to support new rules in the future. --- constraints.py | 44 +++++++++++++ main.py | 168 +++++++++---------------------------------------- rules.py | 119 +++++++++++++++++++++++++++++------ 3 files changed, 176 insertions(+), 155 deletions(-) create mode 100644 constraints.py diff --git a/constraints.py b/constraints.py new file mode 100644 index 0000000..8053162 --- /dev/null +++ b/constraints.py @@ -0,0 +1,44 @@ +import logging + +class Calculator(object): + + def get_position(self, constraints): + """Return a position meeting all the constraints, or None if fails.""" + if len(constraints) is 1 and constraints[0].type is 'Position': + return constraints[0].position + elif (len(constraints) is 2 and constraints[0].type is 'Line' + and constraints[1].type is 'Line'): + return self.__line_line(constraints[0], constraints[1]) + else: + return + + def __line_line(self, line1, line2): + """Find the intersect point of two lines. + Each line is defined by (a,b) thus it would be y=ax+b.""" + a1,b1 = line1.coefficients + a2,b2 = line2.coefficients + if (a1 == a2): + logging.warning('Oops! Parallel lines never intersect! Ignoring one rule') + return + x = (b2 - b1) / (a1 - a2) + y = (a1 * b2 - a2 * b1) / (a1 - a2) + return (x,y) + + +class Position(object): + def __init__(self, position): + self.type = self.__class__.__name__ + self.position = position + + +class Line(object): + def __init__(self, coefficients): + self.type = self.__class__.__name__ + self.coefficients = coefficients + + +class Circle(object): + def __init__(self, center, radius): + self.type = self.__class__.__name__ + self.center = center + self.radius = radius \ No newline at end of file diff --git a/main.py b/main.py index 2b03ee2..5182a13 100644 --- a/main.py +++ b/main.py @@ -1,32 +1,12 @@ import sys -import math import random -import numpy as np import matplotlib.pyplot as plt -import rules import logging logging.basicConfig(level=logging.DEBUG, format='%(message)s') -DEF_ANGLE = '=' -DEF_PERP = '|' -DEF_PARA = '-' -DEFAULT_POSITIONS = [(0,0), (2,0), (1,1)] -TYPES = {'POSITION':0, # '(0,0)' - 'ANGLE':1, # 'ABC=60', 'ABC' (meaning 'ABC=180') - 'PERP':2, # 'AB|BC' - 'PARA':3, # 'AB-CD' - 'UNKNOWN':4} +import rules +import constraints -def gettype(rule): - if ',' in rule: - return TYPES['POSITION'] - if len(rule) == 3 or DEF_ANGLE in rule: - return TYPES['ANGLE'] - if DEF_PERP in rule: - return TYPES['PERP'] - if DEF_PARA in rule: - return TYPES['PARA'] - return TYPES['UNKNOWN'] class RuleParser(object): """A parser to get rules from an input stream. @@ -56,7 +36,7 @@ def parse(self, input): if rule_constructor.rule: for d in newdots: self.dotrules.append( - (d, rule_constructor.rule.description)) + (d, rule_constructor.rule)) self.__addlines(rule_constructor.shapes) def __addlines(self, newlines): @@ -65,6 +45,7 @@ def __addlines(self, newlines): if sorted(new) not in self.lines: self.lines.append(sorted(new)) + class SmartCalc(RuleParser): """Init me with an input stream giving rules, then I'll give you the world @@ -87,16 +68,6 @@ def __getrulesfordot(self, dot): rules.append(r[1]) return rules - def __getdefaultposition(self): - """Return the next position from the list DEFAULT_POSITIONS.""" - if self.__dict__.has_key('dpcount'): - self.dpcount += 1 - if self.dpcount == len(DEFAULT_POSITIONS): - self.dpcount -= len(DEFAULT_POSITIONS) - else: - self.dpcount = 0 - return DEFAULT_POSITIONS[self.dpcount] - def __getrandomposition(self): """Return a random position not too far away from current positions.""" cp = self.positions.values() # current positions for reference @@ -135,118 +106,41 @@ def __getrandompositiononline(self, line): py = line[0] * px + line[1] return (px,py) - def __getlinefromrule(self, dot, rule): - """For a dot, each rule can restrict it to be on a line. - This method calculates a set of parameters (a,b) from a rule, - such that the dot (x,y) must be on y=ax+b.""" - type = gettype(rule) - if type is TYPES['ANGLE']: - if len(rule) == 3: - rotation = 180.0 - else: - rotation = float(rule[4:]) - if rule[0] is dot: - baseline = rule[1:3] # 'ABC' -> 'BC' - basedot = rule[1] - rotation = -rotation - elif rule[2] is dot: - baseline = rule[1::-1] # 'ABC' -> 'BA' - basedot = rule[1] - else: - logging.error('Invalid rule: ' + rule) - return - elif type is TYPES['PERP'] or type is TYPES['PARA']: - if type is TYPES['PERP']: - rotation = 90.0 - else: - rotation = 0.0 - if rule[0] is dot: - basedot = rule[1] - baseline = rule[3:5] - elif rule[1] is dot: - basedot = rule[0] - baseline = rule[3:5] - elif rule[3] is dot: - basedot = rule[4] - baseline = rule[0:2] - elif rule[4] is dot: - basedot = rule[3] - baseline = rule[0:2] - else: - logging.error('Invalid rule: ' + rule) - return - else: - logging.error('Unknown rule: ' + rule) - return - # Now we get baseline, basedot and rotation - x0,y0 = self.positions[basedot] - x1,y1 = self.positions[baseline[0]] - x2,y2 = self.positions[baseline[1]] - if x1 == x2: - if y2 > y1: - theta = math.pi/2 - elif y2 < y1: - theta = -math.pi/2 - else: - logging.error('Invalid rule (identical dots): ' + rule) - return - else: - theta = math.atan((y2 - y1) / (x2 - x1)) - theta += rotation / 180.0 * math.pi - a = math.tan(theta) - b = y0 - a * x0 - logging.info(basedot + dot + ': y=' + str(a) + 'x+' + str(b) \ - + ' (theta=' + str(theta / math.pi * 180) + ')') - return a,b - - def __findintersect(self, lines): - """Find the intersect point of two lines. - Each line is defined by (a,b) thus it would be y=ax+b.""" - a1,b1 = lines[0] - a2,b2 = lines[1] - if (a1 == a2): - logging.warning('Oops! Parallel lines never intersect! Ignoring one rule') - return self.__getrandompositiononline(lines[0]) - x = (b2 - b1) / (a1 - a2) - y = (a1 * b2 - a2 * b1) / (a1 - a2) - return (x,y) - def __calcpositions(self): """Calculate the positions of all dots in self.dots, based on the rules in self.dotrules.""" for dot in self.dots: - freedom = 2 - lines = [] + consts = [] rules = self.__getrulesfordot(dot) - logging.info(dot + ": " + str(rules)) + logging.info("%s: %s" % (dot, rules)) + freedom = 2 for rule in rules: - type = gettype(rule) - if type is TYPES['POSITION']: - x,y = rule[1:-1].split(',') - pos = (float(x), float(y)) - freedom = 0 - if len(rules) > 1: - logging.warning('Position of ' + dot + ' is assigned. ' \ - + 'Other rules will be ignored') - else: - line = self.__getlinefromrule(dot, rule) - if line is not None: - lines.append(line) - freedom -= 1 - # If overdetermined, additional rules would be ignored - if freedom == 0: - break - if freedom == 2: + if rule.degree <= freedom: + consts.append(rule.get_constraint_for_dot(dot, self.positions)) + elif rule.degree is 2 and freedom is 1: + consts = [rule.get_constraint_for_dot(dot, self.positions)] + freedom -= rule.degree + if freedom <= 0: + pos = constraints.Calculator().get_position(consts) + if pos is None and consts[0].type is 'Line': + pos = self.__getrandompositiononline( + consts[0].coefficients) + elif freedom is 1 and consts[0].type is 'Line': + # Underdetermined - generate a semi-random position + pos = self.__getrandompositiononline( + consts[0].coefficients) + elif freedom is 2: # Not determined - generate a random position pos = self.__getrandomposition() - elif freedom == 1: - # Underdetermined - generate a semi-random position - pos = self.__getrandompositiononline(lines[0]) - elif len(lines) == 2: - # Fully determined - calculate the exact position - pos = self.__findintersect(lines) - self.positions[dot] = pos - logging.info('Freedom: ' + str(freedom) + '. Position: ' + str(pos)) + else: + pos = None + if pos: + self.positions[dot] = pos + logging.info('Freedom: %d. Position: %s' % ( + freedom, (self.positions[dot],))) + else: + logging.error('Failed to calculate position. Find the bug!') + def main(): if len(sys.argv) == 2: diff --git a/rules.py b/rules.py index 66ee1d6..6e87bd6 100644 --- a/rules.py +++ b/rules.py @@ -1,4 +1,10 @@ -class RuleConstructor: +import math +import logging + +import constraints + + +class RuleConstructor(object): """Construct a rule and shapes from a string @definition. Attributes: @@ -31,7 +37,10 @@ class Rule(object): description: a string to describe the rule. """ - type = 'base' # Should be overriden by subclasses + # Reduction of degree of freedom that a rule would impose to the + # position of one dot, given the position of all other dots. + # It should be overriden by subclasses. + degree = 0 @classmethod def bingo(cls, description): @@ -41,7 +50,8 @@ def bingo(cls, description): def __init__(self, description): self.description = description - self.type = self.__class__.type + self.type = self.__class__.__name__ + self.degree = self.__class__.degree def get_shapes(self): """Get the shapes implicitly defined in the rule.""" @@ -62,13 +72,38 @@ def are_float(strings): return False return True + @staticmethod + def get_line_by_rotation(pos0, pos1, pos2, rotation): + """Return the coefficients (a,b) of a line that goes through @pos1, + and is rotated by @rotation against the line from @pos1 to @pos2.""" + x0,y0 = pos0 + x1,y1 = pos1 + x2,y2 = pos2 + if x1 == x2: + if y2 > y1: + theta = math.pi/2 + elif y2 < y1: + theta = -math.pi/2 + else: + logging.error('Identical positions') + return + else: + theta = math.atan((y2 - y1) / (x2 - x1)) + theta += rotation / 180.0 * math.pi + a = math.tan(theta) + b = y0 - a * x0 + logging.info('y=' + str(a) + 'x+' + str(b) \ + + ' (theta=' + str(theta / math.pi * 180) + ')') + return a, b + class RulePosition(Rule): """Define the position of a dot. e.g. '(3.1,2.5)' """ - type = 'position' + + degree = 2 __wrap_l = '(' __wrap_r = ')' __separator = ',' @@ -83,10 +118,11 @@ def bingo(cls, description): len(values) is 2 and cls.are_float(values)) - def get_constraint_for_dot(self, dot, positions): + def get_constraint_for_dot(self, dot, positions={}): """Get the constraint for a specified dot in the rule, provided the positions of all other dots.""" - pass + values = self.description[1:-1].split(self.__class__.__separator) + return constraints.Position((float(values[0]), float(values[1]))) class RulePara(Rule): @@ -95,7 +131,7 @@ class RulePara(Rule): e.g. 'AB-CD' """ - type = 'para' + degree = 1 __separator = '-' @classmethod @@ -115,7 +151,24 @@ def get_shapes(self): def get_constraint_for_dot(self, dot, positions): """Get the constraint for a specified dot in the rule, provided the positions of all other dots.""" - pass + if self.description[0] is dot: + basedot = self.description[1] + baseline = self.description[3:5] + elif self.description[1] is dot: + basedot = self.description[0] + baseline = self.description[3:5] + elif self.description[3] is dot: + basedot = self.description[4] + baseline = self.description[0:2] + elif self.description[4] is dot: + basedot = self.description[3] + baseline = self.description[0:2] + else: + logging.error('Rule %s is not for dot %s' % (self.description, dot)) + return + return constraints.Line(self.__class__.get_line_by_rotation( + positions[basedot], positions[baseline[0]], positions[baseline[1]], + 0)) class RulePerp(Rule): @@ -124,27 +177,44 @@ class RulePerp(Rule): e.g. 'AB|CD' """ - type = 'perp' + degree = 1 __separator = '|' @classmethod def bingo(cls, description): """Return True if the description string can be recognized to be - a valid rule of this type.""" + a valid self.description of this type.""" lines = description.split(cls.__separator) return (len(lines) is 2 and len(lines[0]) is 2 and len(lines[1]) is 2) def get_shapes(self): - """Get the shapes implicitly defined in the rule.""" + """Get the shapes implicitly defined in the self.description.""" lines = self.description.split(self.__class__.__separator) return lines def get_constraint_for_dot(self, dot, positions): - """Get the constraint for a specified dot in the rule, + """Get the constraint for a specified dot in the self.description, provided the positions of all other dots.""" - pass + if self.description[0] is dot: + basedot = self.description[1] + baseline = self.description[3:5] + elif self.description[1] is dot: + basedot = self.description[0] + baseline = self.description[3:5] + elif self.description[3] is dot: + basedot = self.description[4] + baseline = self.description[0:2] + elif self.description[4] is dot: + basedot = self.description[3] + baseline = self.description[0:2] + else: + logging.error('Rule %s is not for dot %s' % (self.description, dot)) + return + return constraints.Line(self.__class__.get_line_by_rotation( + positions[basedot], positions[baseline[0]], positions[baseline[1]], + 90)) class RuleAngle(Rule): @@ -153,7 +223,7 @@ class RuleAngle(Rule): e.g. 'ABC=45' """ - type = 'angle' + degree = 1 __separator = '=' @classmethod @@ -173,7 +243,20 @@ def get_shapes(self): def get_constraint_for_dot(self, dot, positions): """Get the constraint for a specified dot in the rule, provided the positions of all other dots.""" - pass + rotation = float(self.description[4:]) + if self.description[0] is dot: + baseline = self.description[1:3] # 'ABC' -> 'BC' + basedot = self.description[1] + rotation = -rotation + elif self.description[2] is dot: + baseline = self.description[1::-1] # 'ABC' -> 'BA' + basedot = self.description[1] + else: + logging.error('Rule %s is not for dot %s' % (self.description, dot)) + return + return constraints.Line(self.__class__.get_line_by_rotation( + positions[basedot], positions[baseline[0]], positions[baseline[1]], + rotation)) class RuleCollinear(RuleAngle): @@ -187,8 +270,8 @@ def bingo(cls, description): return len(description) is 3 def __init__(self, description): + Rule.__init__(self, description) self.description = description + '=180' - self.type = self.__class__.type RuleTypes = [RulePosition, RulePara, RulePerp, RuleAngle, RuleCollinear] @@ -200,8 +283,8 @@ def test(): 'AB|CD', 'ABC', 'ABC=32', - None, '', + None, 'AB', '(1.3a,2.4)', 'ABC-EF', @@ -211,7 +294,7 @@ def test(): 'ABCD=32'] for d in descriptions: print d - rule_constructor = RuleConstructor(d) + self.description_constructor = self.descriptionConstructor(d) if rule_constructor.rule: print '%s --> Type: %s. Shapes: %s' % ( rule_constructor.rule.description,