from lark import Lark, Transformer, v_args import json, sys, re, codecs, os from stats import grades_to_vectors, stu_record_to_vector, courses_to_vector, course_main_record, num from minizinc import Instance, Model, Solver from datetime import datetime from collections import defaultdict import pandas as pd import util debug_out = codecs.open("cache/degrees_debug.txt", "w", "utf-8") def d(s): global debug_out #if type(s) == tuple or type(s) == list: # debug_out.write(" ".join(str(s)) + "\n") debug_out.write(str(s) + "\n") """describe college courses, their number of units, any other courses that are prerequisites, as well as various degrees which consist of groups of courses that must be taken. The groups have rules associated with them, such as: - take all (the following courses) - take n (where n=3 or 5 or something) - take at least n units from the following list (so the taken course's unit value must add up to n or greater) - and so on.""" class CourseOr: def __init__(self, subs): self.names = subs self.name = " or ".join(self.names) def __repr__(self): return f"[{self.name}]" class Course: def __init__(self, name): self.name = name def __repr__(self): return f"{self.name}" class CourseList: def __init__(self, name, courses): self.name = name self.courses = courses def __iter__(self): return iter(self.courses) def __len__(self): return len(self.courses) def __repr__(self): return ", ".join([f"\"{str(x)}\"" for x in self.courses]) class DegreeRule: rule_count = 1 def __init__(self, rule_type, course_list): self.rule_type = rule_type if 'n' in rule_type.__dict__: self.n = rule_type.n self.course_list = course_list self.rule_number = DegreeRule.rule_count DegreeRule.rule_count += 1 def __repr__(self): s = ",".join( [ f"{x}" for x in self.course_list]) the_rule = f"% {self.rule_type}\n" return the_rule + f"array[1..{len(self.course_list)}] of string: rule_{self.rule_number}_courses = [{s}];" class Degree: def __init__(self, name, degree_rules): self.name = name self.degree_rules = degree_rules def __repr__(self): n = 1 s = f"% Degree: {self.name}\n" for r in self.degree_rules: s += str(r) + "\n" n += 1 return s class RuleType(): def __init__(self,what,n=0): self.what = what self.n = n def __repr__(self): return "" #f"RuleType({self.what}, {self.n})" class Rule: pass class TakeAll(Rule): def __repr__(self): return f"Take all:" class TakeN(Rule): def __init__(self, n): self.n = n def __repr__(self): return f"Take {self.n} courses:" class TakeNUnits(Rule): def __init__(self, n): self.n = n def __repr__(self): return f"Take {self.n} units:" @v_args(inline=True) class DSLTransformer(Transformer): def __init__(self): self.courses = {} self.degrees = {} self.lists = {} def course_declaration(self, name, units): d("\ncourse_declaration") d(name) d(units) units_value = float(units.children[0]) # Handle "3", "3.0", etc. #prereq_list = [p.value for p in prereqs if p is not None] course = Course(name.value) course.units = units_value #course.prereqs = prereq_list self.courses[name.value] = course return course def take_all(self): d("\ntake_all") return TakeAll() def take_n_courses(self, n): d("\ntake_n_courses") d(n) return TakeN(n) def take_n_units(self, n): d("\ntake_n_units") d(n) return TakeNUnits(n) def degree_rule(self, rule_type, course_list): d("\ndegree_rule") d(rule_type) d(course_list) return DegreeRule(rule_type, course_list) def list_ref(self, name): d("\nlist_ref") d(name) return self.lists[name] def course(self, c): d("\ncourse") d(c) name = c.value if name not in self.courses: self.courses[name] = Course(name) return self.courses[name] def course_or(self, *items): d("\ncourse_or") d(items) return CourseOr(items) def course_list(self, *items): if items[-1] == None: items = items[:-1] d("\ncourse_list") d(items) return items def list_declaration(self, name, course_list): d("\nlist_declaration") d(name) d(course_list) c = CourseList(name, course_list) self.lists[name] = c return c def program(self, name, *rules): d("\nprogram") d(name) d(rules) dg = Degree(name, rules) self.degrees[name] = dg return dg grammar = """ start: _spec+ _spec: program | course_declaration | list_declaration program: "program" PROGRAMNAME degree_rule* degree_rule: "take" rule_type [course_list | list_ref] list_ref: LISTNAME rule_type: "all from" -> take_all | "at least" INT "units from" -> take_n_units | "at least" INT "courses from" -> take_n_courses | "at least" INT "course from" -> take_n_courses list_declaration: "list" LISTNAME ":" course_list course_declaration: "course" COURSECODE UNITAMOUNT "units" ["prerequisites" (COURSECODE ","?)*] course_list: [course | course_or] ["," [course | course_or]]* course: COURSECODE course_or: COURSECODE "or" COURSECODE COURSECODE: ("A".."Z")+ INT+ ["A".."Z"]* PROGRAMNAME: ("A".."Z" | "a".."z")+ LISTNAME: ("a".."z"["A".."Z" | "a".."z" | INT]+) UNITAMOUNT: NUMUNITS | NUMUNITS "-" NUMUNITS NUMUNITS: INT | INT "." INT %import common.INT %import common.WS %ignore WS """ def parser(): dsl = """ list a1: CMUN1A, CMUN5, CMUN8, CMUN10 list a2: ENGL1A list a3: PHIL2, PHIL4, ENGL1C, CMUN3 list c1: BUS1, ENGL250, ACCT120, BOT100, ACCT121, BOT105B program BusinessAccountingOption take all from CSIS1, CSIS2 take at least 6 units from ACCT120, ACCT20 or ECON20 take at least 3 courses from MUS5, MUS10, BUS1, ENGL250, ACCT120, BOT100, ACCT121, BOT105B take at least 1 course from c1 program CSUGenEd take at least 1 course from a1 take at least 1 course from a2 take at least 1 course from a3 """ do_parse(dsl) def do_parse(dsl): parser = Lark(grammar) #d(parser.parse(dsl).pretty()) #d("\n\n\n") #d("\nTRANSFORMER: ") parser = Lark(grammar) transformer = DSLTransformer() def parse_dsl(dsl): tree = parser.parse(dsl) return transformer.transform(tree) result = parse_dsl(dsl) print(transformer.courses) print() print(transformer.degrees) print() print(transformer.lists) print() [print(c) for c in transformer.courses] print() print("\nDEGREES: ") for deg,i in transformer.degrees.items(): print(str(i)) print() [print(deg) for deg in transformer.lists] word_to_num = {'course':1, 'One':1, 'one':1, 'two':2, 'three':3, 'four':4, 'five':5, 'six':6, 'seven':7, 'eight':8, 'nine':9, 'ten':10, 'eleven':11,} def word2num(word, verbose=0): word = word.lower() ret = word_to_num[word] if word in word_to_num else word if verbose: print(f" word2num({word}) -> {ret}") return ret lab_classes = {} def load_lab_classes(): global lab_classes if lab_classes: return lab_classes for c in json.loads(codecs.open('cache/courses/courses_built.json','r','utf-8').read()).values(): #print(c) if 'min_lab_hour' in c and float(c['min_lab_hour']) > 0: lab_classes[c['dept'] + c['number']] = 1 print(lab_classes) return lab_classes def is_lab_class(c): lab_classes = load_lab_classes() if c in lab_classes: return True return False def is_noncourse_new_section(noncourse_line): from degree_vars import note_true_section, note_false_section for non in note_false_section: if re.search(non, noncourse_line): #print(f"- {noncourse_line}") return False for yes in note_true_section: if re.search(yes, noncourse_line): #print(f"+ {noncourse_line}") return True print(f" -> should this start a new rule/section? [{noncourse_line}]") return False # take at least additional units from CSIS28 rule_lookup = { 'take_all_prereq': ['RN PROGRAM PREREQUISITES', 'PREREQUISITES', ], 'take at least n courses': ['(One) of the following:', '(\d+) courses total', 'ANY (COURSE) NOT USED IN', 'Choose (One) Course:', 'Choose (one) or more', 'Choose (one|two|three) of the classes listed', 'Choose (one|two|three)', 'Choose (4) classes (12 units) from the following list:', 'Choose ([\d\w]+) courses from', 'Choose (\w+) of the following', 'Please select (ONE) course from the options below:', 'LIST [AB]: Select (\d)', 'Select (1)', 'Select (one) of the following REQUIRED CORE', 'SELECT (ONE|TWO) OF THE FOLLOWING', 'Select (one|two|three)', 'Select (\d+) courses', ], 'take at least n units': [ '(\d+) units total', '(\d+) Unit Minimum', 'Any combination totaling (\d+) units', 'Choose (eight|\d+) units', 'Choose (\w+) units from classes listed', 'Choose a minimum of ([\w\d]+) units from', 'Choose a minimum of (\d+) units', 'Choose at least ([\d|\.]+) units', 'Choose eighteen \((18)\) units from the following list:', 'Choose any combination of courses for a minimum of ([\w\d]+) units\:?', 'Choose any combination of the following courses for a minimum of (\d+) units:', 'Choose courses for at least ([\w\d]+) units', 'LIST A \((\d+) units\)', 'LIST B \((\d+) units\)', 'LIST C \- Any course .*\((\d+) units\)', 'Select (\d+) units', 'Select any (\d+)\-\d+ units from the following',], 'electives': ['Electives', 'Recommended electives?:', 'Recommended Elective'], 'take_all': ['RN PROGRAM', 'REQUIRED CORE', 'CORE COURSES', 'CORE Courses', 'Core Major Courses', 'Core Courses :', 'ADDITIONAL REQUIREMENTS', 'REQUIREMENTS:', 'REQUIREMENTS', 'Required:', 'Requirements', 'Core Requirements', 'Required Courses', 'Required Core', 'REQUIRED', 'LVN PROGRAM', 'Student Teaching Practicum', '^LIST A:?$', '^LIST B:$', 'Complete the following courses:', 'Complete the following required courses for 6 units:', 'Complete all of the following courses:', 'Complete all of the following General Education (Cal-GETC) courses:', 'Program Requirements', 'Required Courses:', 'PROGRAM REQUIREMENTS (5 Units)', 'PROGRAM REQUIREMENTS (162 Hours)', 'PROGRAM REQUIREMENTS (5 Units):', 'PROGRAM REQUIREMENTS (162 Hours):', ], } def lookup_rule(line): verbose = 0 for key in rule_lookup.keys(): for each in rule_lookup[key]: m = re.search(each, line) if m: num = None try: if m.group(1): num = m.group(1) except Exception as e: pass if verbose: print(f"line: {line} matched: {each} with {num}") return key,num print(f"** failed to lookup rule: {line}") return None,None def num_units(s): if s == '': return 0 m = re.match(r'^(\d+)\.?(\d+)?\s?(units)?$',s) if m: if m.group(1) and m.group(2): if m.group(2) == '0': return int(m.group(1)) return float(f"{m.group(1)}.{m.group(2)}") elif m.group(1): return int(m.group(1)) else: try: return int(s) except ValueError: return float(s) def examine5(li,award, verbose=0): summary = [x[0] for x in li] if summary[1] in ['and','or'] and summary[3] in ['and','or']: if verbose: print(" - ", summary) return summary[1] return False def examine(li,award, verbose=0): summary = [x[0] for x in li] if summary[1] in ['and','or']: if verbose: print(" - ", summary) return summary[1] return False # given list of courses, pbd like: [c1, c2, c3, or, c4, c5, c6, or, c7, or, c8, c9], # return [c1, c2, [or, c3, c4], c5, [or, c6, c7, c8], c9] # given [c1, 'or', c2, 'or', c3, 'or', c4, 'and', c5, 'and', c6] # return ['or', c1, c2, c3, [ 'and', c4, c5, c6 ]] def check_ands_ors_pbd(award, pbd, verbose=0): if not pbd: return [] i = 0 or_group = [] and_group = [] while i < len(pbd): token = pbd[i][0] if token == 'or': i += 1 # skip 'or', next item is part of or_group continue # will be handled by next loop iteration elif token == 'and': # Start or continue and_group i += 1 if i < len(pbd): and_group.append(pbd[i]) i += 1 else: # If previous token was not 'and', treat as part of or_group if i == 0 or pbd[i-1] != 'and': or_group.append(token) i += 1 result = ['or'] + or_group if and_group: result.append(['and'] + and_group) return result def check_ands_ors_pbd2(award, pbd, verbose=0): summary = [x[0] for x in pbd] #if verbose: print(" ", summary) for i,x in enumerate(summary): desc = '' if x == 'course': desc = f"\t{pbd[i][2]['code']} {pbd[i][2]['name']}" elif x == 'or': desc = '' elif x == 'and': desc = '' else: desc = pbd[i][2] print(f"{x}\t{desc}") result = [] i = 0 while i < len(pbd): #if pbd[i][1]=='21' and pbd[i][2]=='Political Science': # print('here') if pbd[i][0] == 'or': # special case: math 233 or higher if i < len(pbd) and pbd[i+1][2] == 'higher': result[-1][2]['name'] += "OR HIGHER" i += 2 continue # Take last item from result left = result.pop() group = ['or', left, pbd[i+1]] i += 2 if i < len(pbd) and pbd[i][0] == 'or': group.append(pbd[i+1]) i += 2 result.append(group) elif pbd[i][0] == 'and': # Take last item from result left = result.pop() group = ['and', left, pbd[i+1]] i += 2 if i < len(pbd) and pbd[i][0] == 'and': group.append(pbd[i+1]) i += 2 result.append(group) else: result.append(pbd[i]) i += 1 #print(result) return result def check_ands_ors_pbd1(award, pbd, verbose=0): verbose = 1 return_list = [] if verbose: print(f"check_ands_ors_pbd({award}, ...)") summary = [x[0] for x in pbd] #if verbose: print(" ", summary) for i,x in enumerate(summary): desc = '' if x == 'course': desc = f"\t{pbd[i][2]['code']} {pbd[i][2]['name']}" elif x == 'or': desc = '' else: desc = pbd[i][2] print(f"{x}\t{desc}") # iterate through in groups of 5, from 0/1/2/3/4 up to 6/7/8/9/10 # (for length 1. length n: n-2, n-1, n) skip = 0 for i in range(len(pbd)-4): if skip>0: skip -= 1 continue cmd = examine5(pbd[i:i+5], award, verbose) if cmd: skip = 4 return_list.append( [cmd, pbd[i], pbd[i+2], pbd[i+4]] ) else: return_list.append( pbd[i] ) # iterate through in groups of 3, from 0/1/2 up to 3/4/5 # (for length 6. length n: n-2, n-1, n) skip = 0 for i in range(len(pbd)-2): if skip>0: skip -= 1 continue cmd = examine(pbd[i:i+3], award, verbose) if cmd: skip = 2 return_list.append( [cmd, pbd[i], pbd[i+2]] ) else: return_list.append( pbd[i] ) #if verbose: print(return_list) return return_list def build_program_rules(verbose=0): cfile = "cache/courses/courses_active_built.json" pfile = "cache/programs/programs_built.json" courses = json.loads(codecs.open(cfile,'r','utf-8').read()) programs = json.loads(codecs.open(pfile,'r','utf-8').read()) course_spec = [] for index,c in courses.items(): try: d = c['dept'] n = c['number'] name = c['name'] u2 = num_units(c['min_units']) if 'max_units' in c: u1 = num_units(c['max_units']) else: u1 = u2 if u1 == u2: units = {'units': u1} u = u1 else: units = {'min_units': u2, 'max_units': u1} u = f"{u2}-{u1}" course_spec.append(f"course {d}{n} {u} units") except Exception as e: pass #print(e) #print(json.dumps(c,indent=2)) d_out = codecs.open('cache/prog_debug.txt','w','utf-8') def d(s): d_out.write(str(s) + "\n") for index,p in programs.items(): v2 = 0 # print debugging stuff # Each award (degree or certificate) award = p['award'] + " " + p['program_title'] d("\n" + p['award'] + " " + p['program_title']) #print("\n" + award) this_program = p['award'] + " " + p['program_title'] this_rule = "" r = p['requirements'] course_count = 0 # Each numbered chunk (k) in the requirements section for k in sorted(r.keys()): # Each 'program block definition' # 1st is dict with unit totals, rest are lists. requirements_inorder = sorted( r[k][1:], key=lambda x: float(x[1])) reqs_with_or_blocks = check_ands_ors_pbd( award, requirements_inorder ) for each_r in reqs_with_or_blocks: if each_r[0] == 'or': if not this_rule: this_rule = "take_all from " this_rule += ' or '.join( [x[2]['code'] for x in each_r[1:] ] ) # each_r[1][2]['code']} or {each_r[2][2]['code']}, " if each_r[0] == 'and': if not this_rule: this_rule = "take_all from " this_rule += f"{each_r[1][2]['code']} and {each_r[2][2]['code']}, " elif isinstance(each_r, list): #print(each_r) if each_r[0] == 'h3' or (each_r[0]=='noncourse' and is_noncourse_new_section(each_r[2])): # This is a rule title if this_rule and course_count: d(this_rule) # + f" ({course_count}) " course_count = 0 raw_rule = each_r[2] good_rule, num = lookup_rule(raw_rule) if good_rule: #print(f"\t{good_rule}") n = word2num(num) if num else "" if v2: print(f"\tn = {n}") actual_rule = re.sub(r'\sn\s',f' {n} ',good_rule) if v2: print(f"\tactual rule is: {actual_rule}") #this_rule = f"{good_rule} ({n}) [{raw_rule}] from " this_rule = f"{actual_rule} from " else: #print(f"\t{raw_rule}") if not this_rule: this_rule = "take_all from " this_rule = " * " + raw_rule + " " + "from " #elif each_r[0] == 'noncourse': # also a rule title, some kind of sub-rule? # d( f" ++ (noncourse) {each_r[2]}") elif each_r[0] == 'course': # course in a rule if not this_rule: this_rule = "take_all from " is_lab = '' # '[L]' if is_lab_class(each_r[2]['code']) else '' #this_rule += f"{each_r[2]['code']}{is_lab}({each_r[1]}), " this_rule += f"{each_r[2]['code']}{is_lab}, " if v2: print(f"\t\tthis rule is now: {this_rule}") course_count += 1 if course_count: d(this_rule) # + f" ({course_count})" d_out.close() d_in = codecs.open('cache/prog_debug.txt','r','utf-8').readlines() progs = [] this_prog = [] for line in d_in: if line.strip() == '': if this_prog: progs.append(this_prog) this_prog = [] else: this_prog.append(line.strip()) okay = [] notokay = [] for p in progs: ok =1 for line in p: if line[0] == '*': notokay.append(p) ok = 0 continue if ok: okay.append(p) verbose = 1 if verbose: print("\n\n\n\nThese programs are okay:") for p in okay: for l in p: print(l) print() print("\n\nThese programs are not okay:") for p in notokay: for l in p: print(l) print() date_string = datetime.now().strftime("%Y%m%d") codecs.open(f'cache/programs/ruleprogress_{date_string}.json','w','utf-8').write( json.dumps( {'num_okay': len(okay), 'num_notokay': len(notokay), 'okay': okay, 'notokay': notokay}, indent=2) ) print(f"okay: {len(okay)}") print(f"not okay: {len(notokay)}") return okay, notokay #do_parse('\n'.join(course_spec)) def deg_name_to_filename(deg_name): deg_name = re.sub(r'[\-\s]+', '_', deg_name) deg_name = re.sub(r'[\.\:,]','',deg_name) return deg_name.lower() def substitute_template_var(template, var, value): out = [] for L in template: j = re.sub(r'\[\[' + var + r'\]\]', str(value), L) out.append(j) return out all_courses_dict = {} def course_units(course): #print(course) global all_courses_dict if not all_courses_dict: ac = json.loads(codecs.open('cache/courses/courses_built.json','r','utf-8').read()) for c in ac.values(): if 'min_units' in c: c['units'] = num(c['min_units']) # ignoring max units for degree calculation for now... #if 'max_units' in c: # c['units'] += '-' + num(c['max_units']) all_courses_dict[c['dept'] + c['number']] = c elif 'max_units' in c: c['units'] = num(c['max_units']) all_courses_dict[c['dept'] + c['number']] = c return all_courses_dict[course]['units'] def create_degree_mzn(): okay_courses, notokay_courses = build_program_rules(0) for this_course in okay_courses: fn = 'cache/program_check/' + deg_name_to_filename(this_course[0]) + '.mzn' # don't overwrite if os.path.exists(fn): continue out_file = codecs.open(fn, 'w', 'utf-8') print(this_course) print("\t" + fn) #print(okay_courses) #continue mzn_starter = """%% %% [[degreename]] %% %% Check if a student has attained the degree / program %% %% or what courses they'd still need for it include "base_courses.mzn"; %% Courses student took array[1..numcourses] of bool: student_taken; """.split("\n") all_names = ",".join([ f"\"{str(x)}\"" for x in course_main_record() ]) all_units = ",".join([ str(course_units(x)) for x in course_main_record() ]) output = substitute_template_var(mzn_starter, 'allcourses', all_names) output = substitute_template_var(output, 'allunits', all_units) output = substitute_template_var(output, 'numcourses', len(course_main_record())) output = substitute_template_var(output, 'degreename', this_course[0]) out_file.write("\n".join(output)) for r in this_course[1:]: if re.search(r'^electives', r): this_course.remove(r) # # # EACH RULE # for n,rule in enumerate(this_course[1:]): ################### m1 = re.search(r'take at least (\d+) courses? from (.*)$', rule) if m1: num = m1.group(1) crs = m1.group(2).strip(',').split(",") crs = [ re.sub(r'\s+','',x) for x in crs ] print(f"\t{num} courses from {crs}") # take n courses n_courses = """%% %% %% Rule [[n]]: Take [[num]] courses %% [[originalrule]] array[1..numcourses] of bool: rule_[[n]]_courses = [ [[courseboollist]] ]; array[1..numcourses] of var bool: rule_[[n]]_selected; % needed to complete array[1..numcourses] of var bool: rule_[[n]]_needed = [ rule_[[n]]_selected[j] /\ rule_[[n]]_courses[j] /\ (not student_taken[j]) | j in 1..numcourses ]; % Limit selections to this rule constraint forall(j in 1..numcourses)( if rule_[[n]]_courses[j] == false then rule_[[n]]_selected[j] = false endif ); %% The Rule: Take n courses constraint sum(j in 1..numcourses)( bool2int(rule_[[n]]_selected[j])) >= [[num]]; %% n courses """.split("\n") rule_output = substitute_template_var(n_courses, 'n', n) rule_output = substitute_template_var(rule_output, 'num', num) rule_output = substitute_template_var(rule_output, 'originalrule', rule) courselist = ",".join(courses_to_vector(crs)) rule_output = substitute_template_var(rule_output, 'courseboollist', courselist) out_file.write("\n".join(rule_output)) ################### m1 = re.search(r'take at least (\d+) units? from (.*)$', rule) if m1: num = m1.group(1) crs = m1.group(2).strip(',').split(",") crs = [ re.sub(r'\s+','',x) for x in crs ] print(f"\t{num} units from {crs}") # take n courses n_courses = """%% %% %% Rule [[n]]: Take [[num]] units %% [[originalrule]] array[1..numcourses] of bool: rule_[[n]]_courses = [ [[courseboollist]] ]; array[1..numcourses] of var bool: rule_[[n]]_selected; % needed to complete array[1..numcourses] of var bool: rule_[[n]]_needed = [ rule_[[n]]_selected[j] /\ rule_[[n]]_courses[j] /\ (not student_taken[j]) | j in 1..numcourses ]; % Limit selections to this rule constraint forall(j in 1..numcourses)( if rule_[[n]]_courses[j] == false then rule_[[n]]_selected[j] = false endif ); %% Rule - take n units constraint sum(j in 1..numcourses)(bool2float(rule_[[n]]_selected[j]) * all_units[j])>=[[num]]; %% n units """.split("\n") rule_output = substitute_template_var(n_courses, 'n', n) rule_output = substitute_template_var(rule_output, 'num', num) rule_output = substitute_template_var(rule_output, 'originalrule', rule) courselist = ",".join(courses_to_vector(crs)) rule_output = substitute_template_var(rule_output, 'courseboollist', courselist) out_file.write("\n".join(rule_output)) ################### m1 = re.search(r'(take_all|take_all_prereq) from (.*)$', rule) if m1: crs = m1.group(2).strip(',').split(",") crs = [ re.sub(r'\s+','',x) for x in crs ] print(f"\ttake all from {crs}") # take n courses n_courses = """%% %% %% %% Rule [[n]]: Take all courses %% [[originalrule]] array[1..numcourses] of bool: rule_[[n]]_courses = [ [[courseboollist]] ]; array[1..numcourses] of var bool: rule_[[n]]_selected; % needed to complete array[1..numcourses] of var bool: rule_[[n]]_needed = [ rule_[[n]]_selected[j] /\ rule_[[n]]_courses[j] /\ (not student_taken[j]) | j in 1..numcourses ]; % Limit selections to this rule constraint forall(j in 1..numcourses)( if rule_[[n]]_courses[j] == false then rule_[[n]]_selected[j] = false endif ); %% Rule - take all constraint forall(j in 1..numcourses)( if rule_[[n]]_courses[j] == true then rule_[[n]]_selected[j] = true endif ); """.split("\n") rule_output = substitute_template_var(n_courses, 'n', n) rule_output = substitute_template_var(rule_output, 'originalrule', rule) courselist = ",".join(courses_to_vector(crs)) rule_output = substitute_template_var(rule_output, 'courseboollist', courselist) out_file.write("\n".join(rule_output)) ################### # # finish out the file finish = """ %% All Rules: No double dipping constraint forall(j in 1..numcourses)(count([ [[double_dip]] ],true) <= 1); var int: total_courses; var float: total_units; % minimize courses total_courses = sum(j in 1..numcourses)( [[coursesum]] ); % minimize units total_units = sum(j in 1..numcourses)( [[units_sum]] ); solve minimize total_units + total_courses; output [ "{" ] """.split("\n") num_rules = len(this_course[1:]) course_sum = " + ".join( [ f"bool2int(rule_{x}_selected[j])" for x in range(num_rules) ] ) rule_output = substitute_template_var(finish, 'coursesum', course_sum) double_dip = ", ".join( [ f"bool2int(rule_{x}_selected[j])" for x in range(num_rules) ] ) rule_output = substitute_template_var(rule_output, 'double_dip', double_dip) units_sum = " + ".join( [ f"all_units[j] * bool2float(rule_{x}_needed[j])" for x in range(num_rules) ] ) rule_output = substitute_template_var(rule_output, 'units_sum', units_sum) out_file.write("\n".join(rule_output)) display = [ f"""++ [ "\\n\\"rule {x} selected\\": [" ] ++ [ if fix(rule_{x}_selected[j])=true then " \(all_courses[j]), " else "" endif | j in 1..numcourses ] ++ ["],\\n\\"rule {x} need to take\\": [" ] ++ [ if fix(rule_{x}_needed[j])=true then " \(all_courses[j]) ," else "" endif | j in 1..numcourses ] ++ [ "]," ] """ for x in range(num_rules) ] out_file.write("\n".join(display)) out_file.write("""++ ["\\n\\"total units\\": \(total_units)" ] ++ [",\\n\\"total courses\\": \(total_courses) \\n}" ] ; """) out_file.close() def check_student_degrees(): records = grades_to_vectors(boolean=1) i = 0 needed = defaultdict(dict) completion = defaultdict(dict) # took no classes? empty_student = {} for r in records: i += 1 if i>100: break lowest = 1000 lowest_deg = "" recommendation = {} crs = ", ".join([x[0] for x in r[2] ]) if r[0] != "0" and len(r[2]) < 3: print(f"skipping student: {r[0]} taken {crs}") continue else: #print(f"student: {r[0]}, taken {crs}", end="", flush=True) print(f"student: {r[0]}, taken {crs}") for degree in os.listdir('cache/program_check'): m1 = re.match(r'(.*)\.mzn', degree) if m1 and degree != 'base_courses.mzn': try: d = m1.group(1) #print(f"checking student: {r[0]} with degree: {d}, taken {crs}") deg_model = Model(f"cache/program_check/{degree}") #print(".", end="", flush=True) print(f" {d}") # Find the MiniZinc solver configuration for Gecode gecode = Solver.lookup("gecode") # Create an Instance of the n-Queens model for Gecode instance = Instance(gecode, deg_model) # Add the base courses data file #instance = instance.add_file("base_courses.dzn") # Assign student courses taken s = f"student_taken = [" + ','.join( [ str(x) for x in r[1]] ) + "];" instance.add_string(s) result = instance.solve() r_str = str(result).split("\n") r_str = [ re.sub(r'\s?,\s?\]', ']', x) for x in r_str ] r1 = json.loads("\n".join(r_str)) #print(json.dumps(r1,indent=2)) #print() if str(r[0]) == "0": empty_student[d] = r1['total units'] needed[r[0]][d] = r1['total units'] completion[r[0]][d] = r1['total units'] / empty_student[d] if r1['total units']>0 and r1['total units'] < lowest: lowest = r1['total units'] lowest_deg = d recommendation = r1 except Exception as e: print(f"error with student {r[0]} and degree {d}") print(e) print() # What shall we recommend to this student? remaining_courses = [] for k,v in recommendation.items(): if re.search(r'need to take', k): remaining_courses.extend(v) rc = ", ".join(remaining_courses) codecs.open('cache/recommend_program.txt','a','utf-8').write( \ f"student {r[0]} should take {lowest_deg} ({lowest} units)\nAlready taken: {crs}\n" + \ f"needs to take: {rc} \n\n") df = pd.DataFrame.from_dict(needed, orient='index') df.to_csv('cache/program_check/needed.csv') df2 = pd.DataFrame.from_dict(completion, orient='index') df2.to_csv('cache/program_check/needed_percentage.csv') if __name__ == "__main__": options = { 1: ['parsing example',parser] , 2: ['build program rules', build_program_rules], 3: ['create degree logic files', create_degree_mzn], 4: ['check student degrees', check_student_degrees] } print ('') if len(sys.argv) > 1 and re.search(r'^\d+',sys.argv[1]): resp = int(sys.argv[1]) print("\n\nPerforming: %s\n\n" % options[resp][0]) else: print ('') for key in options: print(str(key) + '.\t' + options[key][0]) print('') resp = input('Choose: ') # Call the function in the options dict options[ int(resp)][1]()