Skip to content

Commit

Permalink
Encapsulate rules and constraints, #1
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
cedrusx committed Mar 16, 2016
1 parent 25903f8 commit f31957d
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 155 deletions.
44 changes: 44 additions & 0 deletions constraints.py
Original file line number Diff line number Diff line change
@@ -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
168 changes: 31 additions & 137 deletions main.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
Loading

0 comments on commit f31957d

Please sign in to comment.