diff --git a/source/common/sagemath/checkit.sage b/source/common/sagemath/checkit.sage new file mode 100644 index 000000000..c96677144 --- /dev/null +++ b/source/common/sagemath/checkit.sage @@ -0,0 +1,195 @@ +import sys,json,os,datetime + +# Library of helpful functions +class CheckIt: + @staticmethod + def vars(*latex_names, random_order=True): + """ + Given one or more `latex_names` of strings, returns a tuple + of Sage variables. `random_order` names them so that they appear + in expressions in a random order. + """ + stamp = randrange(100000,999999) + indices = list(range(len(latex_names))) + if random_order: + shuffle(indices) + import string + random_letter = choice(list(string.ascii_lowercase)) + return (var(f"{random_letter}_mi_var_{stamp}_{indices[i]}", latex_name=name) for i, name in enumerate(latex_names)) + + @staticmethod + def shuffled_equation(*terms): + """ + Represents the equation sum(terms)==0, but with terms shuffled randomly + to each side. + """ + new_equation = (SR(0)==0) + for term in terms: + if choice([True,False]): + new_equation += (SR(term)==0) + else: + new_equation += (0==-SR(term)) + return new_equation*choice([-1,1]) + + @staticmethod + def shuffled_inequality(*terms,strict=True): + """ + Represents the equation sum(terms)>0 or >=0, but with terms shuffled randomly + to each side, and random direction of inequality + """ + if choice([True,False]): + if strict: + new_equation = (SR(0)>0) + else: + new_equation = (SR(0)>=0) + for term in terms: + if choice([True,False]): + new_equation += (SR(term)==0) + else: + new_equation += (0==-SR(term)) + else: + if strict: + new_equation = (SR(0)<0) + else: + new_equation = (SR(0)<=0) + for term in terms: + if choice([True,False]): + new_equation += (-SR(term)==0) + else: + new_equation += (0==SR(term)) + return new_equation + + @staticmethod + def latex_system_from_matrix(matrix, variables="x", alpha_mode=False, variable_list=None): + # Augment with zero vector if not already augmented + if not matrix.subdivisions()[1]: + matrix=matrix.augment(zero_vector(QQ, len(matrix.rows())), subdivide=true) + num_vars = matrix.subdivisions()[1][0] + # Start using requested variables + if variable_list is None: + system_vars = [] + else: + system_vars = variable_list + # Conveniently add xyzwv if requested + if alpha_mode: + system_vars += list(var("x y z w v")) + # Finally fall back to x_n as needed + system_vars += [var(f"{variables}_{n+1}") for n in range(num_vars)] + # Build matrix + latex_output = "\\begin{matrix}\n" + for row in matrix.rows(): + if row[0]!= 0: + latex_output += latex(row[0]*system_vars[0]) + previous_terms = True + else: + previous_terms = False + for n,cell in enumerate(row[1:num_vars]): + latex_output += " & " + if cell < 0: + latex_output += " - " + elif cell > 0 and previous_terms: + latex_output += " + " + latex_output += " & " + if cell != 0: + latex_output += latex(cell.abs()*system_vars[n+1]) + if not previous_terms: + previous_terms = bool(cell!=0) + if not previous_terms: + latex_output += " 0 " + latex_output += " & = & " + latex_output += latex(row[num_vars]) + latex_output += "\\\\\n" + latex_output += "\\end{matrix}" + return latex_output + + @staticmethod + def latex_solution_set_from_matrix(matrix): + # Augment with zero vector if not already augmented + if not matrix.subdivisions()[1]: + matrix=matrix.augment(zero_vector(QQ, len(matrix.rows())), subdivide=true) + if (len(matrix.columns())-1) in matrix.pivots(): + return r" \{\} " + solution_dimension = len(matrix.columns())-1 + free_variables = list(var("a b c d e f g h i j")) + kernel_basis=matrix.subdivision(0,0).right_kernel(basis='pivot').basis() + span = sum([kernel_basis[i]*free_variables[i] for i in range(len(kernel_basis))]) + offset = zero_vector(QQ,solution_dimension) + for row_index,col_index in enumerate(matrix.pivots()): + offset[col_index] = matrix.rref().columns()[-1][row_index] + rep = column_matrix(span+offset) + predicate = ",".join([latex(a) for a in free_variables[:len(kernel_basis)]]) + return r" \left\{ " + latex(rep) + r" \,\middle|\, " + predicate + r" \in\mathbb R \right\} " + + @staticmethod + def simple_random_matrix_of_rank(rank,rows=1,columns=1,augmented=False): + # get extra rows and columns, at least zero + extra_rows = max(0,rows-rank) + extra_columns = max(0,columns-rank) + # create matrix with terms between -5 and 5 inclusive, rank in every column, and integer entries RREF + A = random_matrix(QQ,rank+extra_rows,rank,algorithm='echelonizable',rank=rank,upper_bound=6) + # randomly choose pivot indices where dependent columns are injected afterward + inserts = [randrange(rank) for _ in range(extra_columns)] + # pedagogically we want final column to be dependent at least half the time + if extra_columns>0 and choice([True,False]): + inserts[0]=rank-1 + # we'll insert columns backwards to avoid messing up where to inject columns + inserts.sort(reverse=True) + # we won't repeat dependent columns + inserted_columns = [] + for pivot in inserts: + while True: + # get random numbers for pivot rows + rref_pivot_entries = [randrange(-3,4) for _ in range(pivot+1)] + # ensure at least one is nonzero + rref_pivot_entries[randrange(pivot+1)] = randrange(1,4)*choice([-1,1]) + # create vector + dependent_vector = sum([rref_pivot_entries[_]*A.column(_) for _ in range(pivot+1)]) + if dependent_vector not in inserted_columns: + inserted_columns.append(dependent_vector) + A = matrix(A.columns()[:pivot+1]+[dependent_vector]+A.columns()[pivot+1:]).transpose() + break + if augmented: + A.subdivide([],[columns-1]) + return A + +# decorator to help authors avoid confusing .data() with .get_data() in a Generator +def provide_data(func): + return lambda self: func(self.get_data()) + +# BaseGenerator class inherited by each outcome's Generator class to minimize boilerplate +class BaseGenerator: + def __init__(self): + self.__data = None + self.__seed = None + + def data(self): + return {} + + @provide_data + def graphics(data): + return None + + def roll_data(self,seed=None): + if seed is None: + set_random_seed() + seed = randrange(1000) + self.__seed = seed + set_random_seed(seed) + self.__data = self.data() + + def get_data(self): + data = self.__data + data["__seed__"] = f"{self.__seed:04}" + return self.__data + +# converts SageMath objects into latexified strings +# note Python numbers are latexified into strings as well +def json_ready(obj): + if isinstance(obj,str) or isinstance(obj,bool): + return obj + elif isinstance(obj,list): + return [json_ready(item) for item in obj] + elif isinstance(obj,dict): + return {key:json_ready(obj[key]) for key in obj.keys()} + else: + return str(latex(obj)) \ No newline at end of file diff --git a/source/common/sagemath/doenet.sage b/source/common/sagemath/doenet.sage new file mode 100644 index 000000000..fed2f6561 --- /dev/null +++ b/source/common/sagemath/doenet.sage @@ -0,0 +1,6 @@ +def doenet_matrix(mat): + result = "" + for row in mat.rows(): + nums = [str(c) for c in row] + result += f"{' '.join(nums)}" + return result diff --git a/source/linear-algebra/exercises/outcomes/LE/LE1/doenet.sage b/source/linear-algebra/exercises/outcomes/LE/LE1/doenet.sage new file mode 100644 index 000000000..d79375420 --- /dev/null +++ b/source/linear-algebra/exercises/outcomes/LE/LE1/doenet.sage @@ -0,0 +1,18 @@ +load("../../common/sagemath/checkit.sage") +load("../../common/sagemath/doenet.sage") +load("./outcomes/LE/LE1/generator.sage") + +generator = Generator() + +def doenet_option(): + generator.roll_data() + data = generator.get_data() + return f""" + {" ".join(latex(data["vectorequation"]).split())} + {doenet_matrix(data["matrix"])} + """ + +result = "