def test_nC_nP_nT(): from sympy.utilities.iterables import (multiset_permutations, multiset_combinations, multiset_partitions, partitions, subsets, permutations) from sympy.functions.combinatorial.numbers import (nP, nC, nT, stirling, _multiset_histogram, _AOP_product) from sympy.combinatorics.permutations import Permutation from sympy.core.numbers import oo from random import choice c = string.ascii_lowercase for i in range(100): s = ''.join(choice(c) for i in range(7)) u = len(s) == len(set(s)) try: tot = 0 for i in range(8): check = nP(s, i) tot += check assert len(list(multiset_permutations(s, i))) == check if u: assert nP(len(s), i) == check assert nP(s) == tot except AssertionError: print(s, i, 'failed perm test') raise ValueError() for i in range(100): s = ''.join(choice(c) for i in range(7)) u = len(s) == len(set(s)) try: tot = 0 for i in range(8): check = nC(s, i) tot += check assert len(list(multiset_combinations(s, i))) == check if u: assert nC(len(s), i) == check assert nC(s) == tot if u: assert nC(len(s)) == tot except AssertionError: print(s, i, 'failed combo test') raise ValueError() for i in range(1, 10): tot = 0 for j in range(1, i + 2): check = nT(i, j) tot += check assert sum(1 for p in partitions(i, j, size=True) if p[0] == j) == check assert nT(i) == tot for i in range(1, 10): tot = 0 for j in range(1, i + 2): check = nT(range(i), j) tot += check assert len(list(multiset_partitions(list(range(i)), j))) == check assert nT(range(i)) == tot for i in range(100): s = ''.join(choice(c) for i in range(7)) u = len(s) == len(set(s)) try: tot = 0 for i in range(1, 8): check = nT(s, i) tot += check assert len(list(multiset_partitions(s, i))) == check if u: assert nT(range(len(s)), i) == check if u: assert nT(range(len(s))) == tot assert nT(s) == tot except AssertionError: print(s, i, 'failed partition test') raise ValueError() # tests for Stirling numbers of the first kind that are not tested in the # above assert [stirling(9, i, kind=1) for i in range(11) ] == [0, 40320, 109584, 118124, 67284, 22449, 4536, 546, 36, 1, 0] perms = list(permutations(range(4))) assert [ sum(1 for p in perms if Permutation(p).cycles == i) for i in range(5) ] == [0, 6, 11, 6, 1] == [stirling(4, i, kind=1) for i in range(5)] # http://oeis.org/A008275 assert [ stirling(n, k, signed=1) for n in range(10) for k in range(1, n + 1) ] == [ 1, -1, 1, 2, -3, 1, -6, 11, -6, 1, 24, -50, 35, -10, 1, -120, 274, -225, 85, -15, 1, 720, -1764, 1624, -735, 175, -21, 1, -5040, 13068, -13132, 6769, -1960, 322, -28, 1, 40320, -109584, 118124, -67284, 22449, -4536, 546, -36, 1 ] # https://en.wikipedia.org/wiki/Stirling_numbers_of_the_first_kind assert [stirling(n, k, kind=1) for n in range(10) for k in range(n + 1)] == [ 1, 0, 1, 0, 1, 1, 0, 2, 3, 1, 0, 6, 11, 6, 1, 0, 24, 50, 35, 10, 1, 0, 120, 274, 225, 85, 15, 1, 0, 720, 1764, 1624, 735, 175, 21, 1, 0, 5040, 13068, 13132, 6769, 1960, 322, 28, 1, 0, 40320, 109584, 118124, 67284, 22449, 4536, 546, 36, 1 ] # https://en.wikipedia.org/wiki/Stirling_numbers_of_the_second_kind assert [stirling(n, k, kind=2) for n in range(10) for k in range(n + 1)] == [ 1, 0, 1, 0, 1, 1, 0, 1, 3, 1, 0, 1, 7, 6, 1, 0, 1, 15, 25, 10, 1, 0, 1, 31, 90, 65, 15, 1, 0, 1, 63, 301, 350, 140, 21, 1, 0, 1, 127, 966, 1701, 1050, 266, 28, 1, 0, 1, 255, 3025, 7770, 6951, 2646, 462, 36, 1 ] assert stirling(3, 4, kind=1) == stirling(3, 4, kind=1) == 0 raises(ValueError, lambda: stirling(-2, 2)) def delta(p): if len(p) == 1: return oo return min(abs(i[0] - i[1]) for i in subsets(p, 2)) parts = multiset_partitions(range(5), 3) d = 2 assert (sum(1 for p in parts if all(delta(i) >= d for i in p)) == stirling(5, 3, d=d) == 7) # other coverage tests assert nC('abb', 2) == nC('aab', 2) == 2 assert nP(3, 3, replacement=True) == nP('aabc', 3, replacement=True) == 27 assert nP(3, 4) == 0 assert nP('aabc', 5) == 0 assert nC(4, 2, replacement=True) == nC('abcdd', 2, replacement=True) == \ len(list(multiset_combinations('aabbccdd', 2))) == 10 assert nC('abcdd') == sum(nC('abcdd', i) for i in range(6)) == 24 assert nC(list('abcdd'), 4) == 4 assert nT('aaaa') == nT(4) == len(list(partitions(4))) == 5 assert nT('aaab') == len(list(multiset_partitions('aaab'))) == 7 assert nC('aabb' * 3, 3) == 4 # aaa, bbb, abb, baa assert dict(_AOP_product((4, 1, 1, 1))) == { 0: 1, 1: 4, 2: 7, 3: 8, 4: 8, 5: 7, 6: 4, 7: 1 } # the following was the first t that showed a problem in a previous form of # the function, so it's not as random as it may appear t = (3, 9, 4, 6, 6, 5, 5, 2, 10, 4) assert sum(_AOP_product(t)[i] for i in range(55)) == 58212000 raises(ValueError, lambda: _multiset_histogram({1: 'a'}))
def stem(self, objects = None, a_type = None ): """ Here we will rearrange objects which are not all distinct. Named Parameters: objects -- These are the type of objects being rearranged. ('words', 'marbles', 'wordlike'). 'wordlike' is really just like marbles, just a sequence of letters instead of marbles. The difference is just in how the question can be asked. a_type -- The answer type, either "FR" (free response) or "MC" (multiple choice) """ kwargs = { 'objects': objects, 'a_type': a_type } # Throw away problems whos answers are bigger than THREASH, but keep them in the done list INF = float('inf') # For now just take anything THREASH = 5000000 # I unset this for the 'words' type # ERR_THREASH -- Throw awway error options such that (error - answer) > ERR_THREASH*answer ERR_THREASH = 4 # These are the current possible values of the objects option OBJECT_TYPES = ["words", "marbles","wordlike"] if objects == None: objects = random.choice(OBJECT_TYPES) if a_type == None: a_type = random.choice(["FR", "MC"]) # Some words with repeating letters WORDS = ["mathematics", "sleepless", "senseless", "carelessness"] WORDS += ["sleeplessness", "bubblebath", "senescence", "tweedledee"] WORDS += ["senselessness", "massless", "scissors", "pulchritude"] WORDS += ["losslessness", "inhibition", "knickknack","sweettooth" ] COLORS = ["red", "green", "blue", "orange", "yellow", "pink", "clear"] question_stem = "" def make_denom(l): """ Takes a list [3,4,5] and outputs a string "3!4!5!" and the corresponding value. Parameters: l -- A list of integers Returns -- A pair (str, int) representing the denominator string representation and value. """ # The denominator in the permutation formula l = [i for i in l if i != 1] ls = map(str, l) denom_string = '!\\,'.join(ls) + "!" denom_val = reduce(lambda x,y: x*y, map(sym.factorial, l)) return denom_string, denom_val def make_errors(a, n, l): """ This could probably be improved. For now it generates some alternate answers for multiple choice, that on't look ridiculous. Parameters: a -- is the correct answer n -- is the number of actual objects l -- is the list of group sizes """ l_ = l[:] errors = [] while sum(l_) < n: l_.append(1) l_ = sorted(l_, key = lambda x: -x) for k in range(len(l_)): for i in [-1, 1]: for j in [-1, 1, 0]: if l_[k] + j > 1: l_[k] = l_[k] + j _, denom_val = make_denom(l_) ans = sym.factorial(max(sum(l_), n + i)) / denom_val if ans not in errors and ans != a and np.abs(a - ans) < ERR_THREASH * a: #if ans not in errors and ans != a: errors.append(ans) for i in [0.2, 0.3]: errors.append(int(a*(1 + i))) errors.append(int(a*(1-i))) random.shuffle(errors) return errors[0:4] def parse_word(word): """ This convers a word into a dictionary of letters and counts and also produces a sentence describing the situation. Examples: parse_word("teepee) returns: ({'e': 4}, "There are 4 e's in teepee", [4]) parse_word("sleeplessness") returns ({'e': 4, 'l': 2, 's': 5}, "There are 5 s's, 4 e's, and 2 l's in sleeplessness", [5, 4, 2]) Parameters: word -- a word Returns -- (dict, string, list) see above for example """ word_dict = {} for k,g in iter.groupby(sorted(word), lambda x: x): count = len(list(g)) if count > 1: word_dict[k] = count l = ["%s %s's" % (word_dict[key], key) for key in word_dict] word_dict_string = "There are " + tools.serialize(*l) + " in \"" + word + "\"" word_letter_counts = list(word_dict.values()) return word_dict, word_dict_string, word_letter_counts if objects == None: objects = random.choice(OBJECT_TYPES) if objects == "words": THREASH = INF #This should not ba set for actual words word = random.choice(WORDS) num_objects = len(word) word_dict, word_string, group_counts = parse_word(word) item = (objects, group_counts) if item in self.done: return self.stem(**kwargs) self.done.append(item) denom_string, denom_val = make_denom(group_counts) answer = nP(word, len(word)) question_stem = "How many distinct rearrangements of the letters in \"" + word \ + "\" are there?" explanation = "%s. So the number of distinct rearrangements of %s is:" % (word_string, word) explanation += "$$\\frac{%s!}{%s} = %s$$" %(len(word), denom_string, answer) elif objects == "marbles": # Get case correct def marble_string(i): if i == 1: return "marble" else: return "marbles" random.shuffle(COLORS) # You will have 3 - 5 groups with the distribution indicated num_groups = random.choice([3,3,4,4,4,5]) # Each group will have between 1 to 5 members group_counts = [random.choice([2,2,3,3,3,4,4,5]) for i in range(num_groups)] num_objects = sum(group_counts) marble_dict = dict([(COLORS[i], group_counts[i]) for i in range(len(group_counts))]) print marble_dict, num_objects item = (objects, sorted(group_counts)) if item in self.done: return self.stem(**kwargs) self.done.append(item) marble_string = tools.serialize(*["%s %s %s" % (group_counts[i], COLORS[i], marble_string(group_counts[i])) for i in range(num_groups)]) denom_string, denom_val = make_denom(group_counts) answer = nP(marble_dict, num_objects) # If number is too big try again if answer > THREASH: return self.stem(**kwargs) question_stem = "How many distint ways are there to arrange %s in a row?" % (marble_string) explanation = "The number of distinct rearrangements of the marbles is:" explanation += "$$\\frac{%s!}{%s} = %s$$" %(num_objects, denom_string, answer) elif objects == 'wordlike': # This code is repeated ... yuck # You will have 3 - 5 groups with the distribution indicated num_groups = random.choice([3,3,4,4,4,5]) # Each group will have between 1 to 5 members group_counts = [random.choice([1] + [2]*2 + [3]*3 + [4]*2 + [5]) for i in range(num_groups)] num_objects = sum(group_counts) item = (objects, sorted(group_counts)) if item in self.done: return self.stem(**kwargs) self.done.append(item) def build_string(group_counts): alph = list('ABCDEFGHIJKLMNIPQRSTUVWXYZ') random.shuffle(alph) strg = [] for i in group_counts: strg += alph.pop()*i random.shuffle(strg) return ''.join(strg) strg = build_string(group_counts) denom_string, denom_val = make_denom(group_counts) answer = nP(strg, len(strg)) # If number is too big try again if answer > THREASH: return self.stem(**kwargs) question_stem = "How many distint rearrangements of the string \'%s\' are there?" % (strg) explanation = "The number of distinct rearrangements of the string \'%s\' is:" % (strg) explanation += "$$\\frac{%s!}{%s} = %s$$" %(num_objects, denom_string, answer) else: print "Invald a_type: " + a_type if a_type == "FR": question_stem += " Give your answer as an integer." answer_mathml = tools.fraction_mml(answer) return tools.fully_formatted_question(question_stem, explanation, answer_mathml) else: errors = make_errors(answer, num_objects, group_counts) distractors = [answer] + errors distractors = ["$_%s$_" % sym.latex(distractor) for distractor in distractors] return tools.fully_formatted_question(question_stem, explanation, answer_choices=distractors)
def test_nC_nP_nT(): from sympy.utilities.iterables import ( multiset_permutations, multiset_combinations, multiset_partitions, partitions, subsets, permutations) from sympy.functions.combinatorial.numbers import ( nP, nC, nT, stirling, _multiset_histogram, _AOP_product) from sympy.combinatorics.permutations import Permutation from sympy.core.numbers import oo from random import choice c = string.ascii_lowercase for i in range(100): s = ''.join(choice(c) for i in range(7)) u = len(s) == len(set(s)) try: tot = 0 for i in range(8): check = nP(s, i) tot += check assert len(list(multiset_permutations(s, i))) == check if u: assert nP(len(s), i) == check assert nP(s) == tot except AssertionError: print(s, i, 'failed perm test') raise ValueError() for i in range(100): s = ''.join(choice(c) for i in range(7)) u = len(s) == len(set(s)) try: tot = 0 for i in range(8): check = nC(s, i) tot += check assert len(list(multiset_combinations(s, i))) == check if u: assert nC(len(s), i) == check assert nC(s) == tot if u: assert nC(len(s)) == tot except AssertionError: print(s, i, 'failed combo test') raise ValueError() for i in range(1, 10): tot = 0 for j in range(1, i + 2): check = nT(i, j) tot += check assert sum(1 for p in partitions(i, j, size=True) if p[0] == j) == check assert nT(i) == tot for i in range(1, 10): tot = 0 for j in range(1, i + 2): check = nT(range(i), j) tot += check assert len(list(multiset_partitions(range(i), j))) == check assert nT(range(i)) == tot for i in range(100): s = ''.join(choice(c) for i in range(7)) u = len(s) == len(set(s)) try: tot = 0 for i in range(1, 8): check = nT(s, i) tot += check assert len(list(multiset_partitions(s, i))) == check if u: assert nT(range(len(s)), i) == check if u: assert nT(range(len(s))) == tot assert nT(s) == tot except AssertionError: print(s, i, 'failed partition test') raise ValueError() # tests for Stirling numbers of the first kind that are not tested in the # above assert [stirling(9, i, kind=1) for i in range(11)] == [ 0, 40320, 109584, 118124, 67284, 22449, 4536, 546, 36, 1, 0] perms = list(permutations(range(4))) assert [sum(1 for p in perms if Permutation(p).cycles == i) for i in range(5)] == [0, 6, 11, 6, 1] == [ stirling(4, i, kind=1) for i in range(5)] # http://oeis.org/A008275 assert [stirling(n, k, signed=1) for n in range(10) for k in range(1, n + 1)] == [ 1, -1, 1, 2, -3, 1, -6, 11, -6, 1, 24, -50, 35, -10, 1, -120, 274, -225, 85, -15, 1, 720, -1764, 1624, -735, 175, -21, 1, -5040, 13068, -13132, 6769, -1960, 322, -28, 1, 40320, -109584, 118124, -67284, 22449, -4536, 546, -36, 1] # http://en.wikipedia.org/wiki/Stirling_numbers_of_the_first_kind assert [stirling(n, k, kind=1) for n in range(10) for k in range(n+1)] == [ 1, 0, 1, 0, 1, 1, 0, 2, 3, 1, 0, 6, 11, 6, 1, 0, 24, 50, 35, 10, 1, 0, 120, 274, 225, 85, 15, 1, 0, 720, 1764, 1624, 735, 175, 21, 1, 0, 5040, 13068, 13132, 6769, 1960, 322, 28, 1, 0, 40320, 109584, 118124, 67284, 22449, 4536, 546, 36, 1] # http://en.wikipedia.org/wiki/Stirling_numbers_of_the_second_kind assert [stirling(n, k, kind=2) for n in range(10) for k in range(n+1)] == [ 1, 0, 1, 0, 1, 1, 0, 1, 3, 1, 0, 1, 7, 6, 1, 0, 1, 15, 25, 10, 1, 0, 1, 31, 90, 65, 15, 1, 0, 1, 63, 301, 350, 140, 21, 1, 0, 1, 127, 966, 1701, 1050, 266, 28, 1, 0, 1, 255, 3025, 7770, 6951, 2646, 462, 36, 1] assert stirling(3, 4, kind=1) == stirling(3, 4, kind=1) == 0 raises(ValueError, lambda: stirling(-2, 2)) def delta(p): if len(p) == 1: return oo return min(abs(i[0] - i[1]) for i in subsets(p, 2)) parts = multiset_partitions(range(5), 3) d = 2 assert (sum(1 for p in parts if all(delta(i) >= d for i in p)) == stirling(5, 3, d=d) == 7) # other coverage tests assert nC('abb', 2) == nC('aab', 2) == 2 assert nP(3, 3, replacement=True) == nP('aabc', 3, replacement=True) == 27 assert nP(3, 4) == 0 assert nP('aabc', 5) == 0 assert nC(4, 2, replacement=True) == nC('abcdd', 2, replacement=True) == \ len(list(multiset_combinations('aabbccdd', 2))) == 10 assert nC('abcdd') == sum(nC('abcdd', i) for i in range(6)) == 24 assert nC(list('abcdd'), 4) == 4 assert nT('aaaa') == nT(4) == len(list(partitions(4))) == 5 assert nT('aaab') == len(list(multiset_partitions('aaab'))) == 7 assert nC('aabb'*3, 3) == 4 # aaa, bbb, abb, baa assert dict(_AOP_product((4,1,1,1))) == { 0: 1, 1: 4, 2: 7, 3: 8, 4: 8, 5: 7, 6: 4, 7: 1} # the following was the first t that showed a problem in a previous form of # the function, so it's not as random as it may appear t = (3, 9, 4, 6, 6, 5, 5, 2, 10, 4) assert sum(_AOP_product(t)[i] for i in range(55)) == 58212000 raises(ValueError, lambda: _multiset_histogram({1:'a'}))
def stem(self, q_type = None, a_type = None, include_PC = False, seed = datetime.datetime.now().microsecond, perm_style = None, choose_style = None): """ There are three possible types of problems: F = factorials, e.g. 5!, P = permutations, e.g. 5!/3! = (5 - 2)!, and C = combinations, e.g. 5!/(3!2!). Named Parameters: q_type -- "Fact", "Perm", or "Comb" for factorial, permutation, or combination a_type -- "MC" or "FR" for multiplechoice and free response include_PC -- This is a boolean. If true include mention of permutations/combinations in both problems and explanations. perm_style -- perm_styles = ['P(%s,%s)', '{}_%s P_%s'], if choose_style is < len(choose_styles), then it chooses the associated style, else it chooses randomly choose_style -- choose_styles = ['{%s \\choose %s}', 'C(%s,%s)', '{}_%s C_%s'], if choose_style is < len(choose_styles), then it chooses the associated style, else it chooses randomly seed -- A seed for the random ops, to make reproducible output """ # Assume if the user sets the perm_style or choose_style, assume include_PC should be True if perm_style is not None or choose_style is not None: include_PC = True kwargs = { 'q_type': q_type, 'a_type': a_type, 'include_PC': include_PC, 'perm_style': perm_style, 'choose_style': choose_style, } # These numbers can get big lets set a threashold THREASH = 300000 # If we exceed the threashold restart with current settings ## I wish I had a better wa to get current values??? def check(): if answer > THREASH: return self.stem(**kwargs) else: pass q_type_orig = q_type a_type_orig = a_type # About 1/5 simple factorials, 2/5 perms, 3/5 combinations if q_type == None: q_type = random.choice(["Fact"] + ["Perm"] * 2 + ["Comb"] * 3) if a_type == None: a_type = random.choice(["MC", "FR"]) # Actually set the data for the problem if q_type == "Fact": N = random.randint(5,7) # Change these to modify range R = 0 else: N = random.randint(7,11) # Change these to modify range R = random.randint(3, N - 4) # To make the explanations work it is good to have N - R > 3 # Choose the "choose" --- Oh have to be a little tricky here since at one place # we have choose(n,k) and another we have choose(%s,$s)%(power,x_pow) choose_styles = ['{%s \\choose %s}', 'C(%s,%s)', '{}_{%s} C_{%s}'] perm_styles = ['P(%s,%s)', '{}_{%s} P_{%s}'] if choose_style not in range(len(choose_styles)): choose_ = random.choice(choose_styles) else: choose_ = choose_styles[choose_style] # Now choose the "perm" if perm_style not in range(len(perm_styles)): perm_ = random.choice(perm_styles) else: perm_ = perm_styles[perm_style] def choose(a ,b): if q_type == "Comb": choice = choose_ % (a, b) else: choice = perm_ % (a, b) return choice q_data = (q_type, N, R) # This determines the problem if q_data in self.done: return self.stem(q_type_orig, a_type_orig, include_PC) # Try again else: self.done.append(q_data) # Mark this data as being used # If include_PC is True, this will be overriden below. question_stem_options = ["Evaluate the following expression involving factorials."] question_stem = random.choice(question_stem_options) explanation = "Recall the definition: " if q_type == "Fact": answer = sym.factorial(N) check() if a_type == "MC": errors = [sym.factorial(N+1), sym.factorial(N-1), sym.factorial(N)/sym.factorial(random.randint(1,N-1))] question_stem += "$$%s!$$" % (N) explanation += "$_%s! = %s = %s$_" % (N, FactCombPerm.format_fact(N), answer) elif q_type == "Perm": if include_PC == True: question_stem = "Evaluate the following permutation." answer = nP(N, N - R) check() if a_type == "MC": errors = list(set([nP(N + i, N - (R + i)) for i in range(-3,4) if i != 0])) errors = [e for e in errors if e != answer] random.shuffle(errors) errors = errors[:4] # This provides 4 distractions if include_PC: explanation_prefix = "%s = \\frac{%s!}{(%s - %s)!} =" % (choose(N, N - R), N, N, N - R) else: explanation_prefix = "" explanation += "$_%s\\frac{%s!}{%s!} = \\frac{%s}{%s} = %s = %s$_" \ % (explanation_prefix, N, R, FactCombPerm.format_fact(N), FactCombPerm.format_fact(R), FactCombPerm.format_fact(N, R+1), answer) if include_PC: question_stem += "$$%s$$" %(choose(N, N - R)) else: question_stem += "$$\\frac{%s!}{%s!}$$" % (N, R) else: if include_PC == True: question_stem = "Evaluate the following combination." answer = nC(N, R) check() if a_type == "MC": errors = list(set([nC(N + i, R + i // 2) for i in range(-3,4) if i != 0 and R + i // 2 > 0])) errors = [e for e in errors if e != answer] random.shuffle(errors) errors = errors[:4] # This provides 4 distractions if include_PC: explanation_prefix = "%s = \\frac{%s!}{%s!\\,(%s - %s)!} = " % (choose(N, R), N, R, N, R) else: explanation_prefix = "" if R >= N - R: explanation += tools.align("%s\\frac{%s!}{%s!\,%s!}" \ % (explanation_prefix, N, R, N-R), "\\frac{%s}{(%s)(%s)}" \ % (FactCombPerm.format_fact(N), FactCombPerm.format_fact(R), FactCombPerm.format_fact(N-R)), "\\frac{%s}{%s} = %s" \ % (FactCombPerm.format_fact(N, R + 1), FactCombPerm.format_fact(N - R), answer)) else: explanation += tools.align("%s\\frac{%s!}{%s!\,%s!}" \ % (explanation_prefix, N, R, N-R), "\\frac{%s}{(%s)(%s)}" \ % (FactCombPerm.format_fact(N), FactCombPerm.format_fact(R), FactCombPerm.format_fact(N-R)), "\\frac{%s}{%s} = %s" \ % (FactCombPerm.format_fact(N, (N - R) + 1), FactCombPerm.format_fact(R), answer)) if include_PC: question_stem += "$$%s$$" % (choose(N, R)) else: question_stem += "$$\\frac{%s!}{%s!\\,%s!}$$" % (N, R, N - R) explanation += "<br>" if a_type == "FR": question_stem += "Give your answer as an integer." answer_mathml = tools.fraction_mml(answer) return tools.fully_formatted_question(question_stem, explanation, answer_mathml) else: distractors = [answer] + errors distractors = ["$_%s$_" % sym.latex(distractor) for distractor in distractors] return tools.fully_formatted_question(question_stem, explanation, answer_choices=distractors)