923 lines
31 KiB
Python
923 lines
31 KiB
Python
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
|
|
|
|
|
|
debug_out = codecs.open("cache/degrees_debug.txt", "w", "utf-8")
|
|
|
|
def d(s):
|
|
#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)
|
|
|
|
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)
|
|
return Course(c.value)
|
|
|
|
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 ([\d\w]+) courses from',
|
|
'Choose (\w+) of the following',
|
|
'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',
|
|
'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 any combination of courses for a minimum of ([\w\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?:', ],
|
|
'take_all': ['RN PROGRAM', 'REQUIRED CORE', 'CORE COURSES', 'ADDITIONAL REQUIREMENTS','REQUIREMENTS:',
|
|
'Requirements', 'Core Requirements',
|
|
'Required Core', 'REQUIRED', 'LVN PROGRAM', 'Student Teaching Practicum', '^LIST A:?$', '^LIST B:$',
|
|
'Program Requirements', 'Required Courses:', '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
|
|
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 examine(li,award, verbose=0):
|
|
summary = [x[0] for x in li]
|
|
if summary[1] in ['and','or']:
|
|
if verbose: print(" - ", summary)
|
|
|
|
|
|
def check_ands_ors_pbd(award, pbd, verbose=0):
|
|
verbose = 0
|
|
if verbose: print(f"check_ands_ors_pbd({award}, ...)")
|
|
summary = [x[0] for x in pbd]
|
|
if verbose: print(" ", summary)
|
|
|
|
# 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)
|
|
for i in range(len(pbd)-2):
|
|
examine(pbd[i:i+3], award, verbose)
|
|
if verbose: print()
|
|
|
|
|
|
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.
|
|
|
|
check_ands_ors_pbd( award, sorted( r[k][1:], key=lambda x: float(x[1])) )
|
|
for each_r in sorted( r[k][1:], key=lambda x: float(x[1])):
|
|
|
|
if each_r[0] in ['and','or']:
|
|
#print(' ', each_r[0],each_r[1])
|
|
pass
|
|
if 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)
|
|
|
|
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]()
|
|
|