progress on degree checking

This commit is contained in:
Coding with Peter 2023-06-13 09:11:59 -07:00
parent 055a561e18
commit 2a15748d5a
6 changed files with 486 additions and 70 deletions

View File

@ -850,8 +850,8 @@ def enroll_id_list_to_shell(id_list, shell_id, v=0):
def enroll_stem_students_live():
the_term = '178'
do_removes = 0
the_term = '179' # su23 fa23 = 180
do_removes = 1
depts = "MATH BIO CHEM CSIS PHYS PSCI GEOG ASTR ECOL ENVS ENGR".split(" ")
users_to_enroll = users_in_depts_live(depts, the_term) # term id
@ -863,7 +863,6 @@ def enroll_stem_students_live():
print("\n\nALREADY IN STEM SHELL %s" % str(users_in_stem_shell))
enroll_us = users_to_enroll.difference(users_in_stem_shell)
#enroll_us = users_to_enroll
remove_us = users_in_stem_shell.difference(users_to_enroll)
print("\n\nTO ENROLL %s" % str(enroll_us))

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,10 @@
from lark import Lark, Transformer, v_args
import json, sys, re, codecs
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")
@ -314,6 +319,8 @@ def is_noncourse_new_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': ['(\d+) courses total', 'SELECT (ONE|TWO) OF THE FOLLOWING', 'Select (one|two|three)', 'Select (\d+) courses',
@ -321,9 +328,10 @@ rule_lookup = {
'Choose (\w+) of the following','Choose (one|two|three)', 'Choose ([\d\w]+) courses from',
'ANY (COURSE) NOT USED IN', 'Select (1)', 'Select (one) of the following REQUIRED CORE',
'(One) of the following:', 'LIST [AB]: Select (\d)', 'Choose (One) Course:', ],
'take at least n units': ['LIST A \((\d+) units\)', 'LIST B \((\d+) units\)', 'LIST C \- Any course .*\((\d+) units\)', '(\d+) units total', 'Select (\d+) units', 'Any combination totaling (\d+) units',
'take at least n units': ['LIST A \((\d+) units\)', 'LIST B \((\d+) units\)', 'LIST C \- Any course .*\((\d+) units\)', '(\d+) units total',
'Select (\d+) units', 'Any combination totaling (\d+) units',
'Choose (\w+) units from classes listed', 'Choose a minimum of ([\w\d]+) units from',
'Choose any combination of courses for a minimum of ([\w\d]+) units',
'Choose any combination of courses for a minimum of ([\w\d]+) units\:?',
'Choose ([\w\d]+) units', 'Choose courses for at least ([\w\d]+) units',
'Choose a minimum of (\d+) units', 'Select any (\d+)\-\d+ units from the following'],
'electives': ['Electives', 'Recommended electives?:', ],
@ -335,6 +343,8 @@ rule_lookup = {
def lookup_rule(line):
verbose = 0
if re.search(rule_lookup['take at least n units'][8], line):
print(f"line: {line} matched: {rule_lookup['take at least n units'][8]}")
for key in rule_lookup.keys():
for each in rule_lookup[key]:
m = re.search(each, line)
@ -349,6 +359,25 @@ def lookup_rule(line):
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']:
@ -368,7 +397,7 @@ def check_ands_ors_pbd(award, pbd, verbose=0):
if verbose: print()
def build_program_rules():
def build_program_rules(verbose=0):
cfile = "cache/courses/courses_active_built.json"
pfile = "cache/programs/programs_built.json"
@ -410,7 +439,7 @@ def build_program_rules():
# Each award (degree or certificate)
award = p['award'] + " " + p['program_title']
d("\n" + p['award'] + " " + p['program_title'])
print("\n" + award)
#print("\n" + award)
this_program = p['award'] + " " + p['program_title']
this_rule = ""
r = p['requirements']
@ -455,7 +484,7 @@ def build_program_rules():
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 ''
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}")
@ -488,32 +517,367 @@ def build_program_rules():
if ok:
okay.append(p)
print("\n\n\n\nThese programs are okay:")
for p in okay:
for l in p:
print(l)
print()
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()
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
do_parse('\n'.join(course_spec))
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
%%
%% Set up all courses
int: numcourses = [[numcourses]];
array[1..numcourses] of string:
all_courses = [ [[allcourses]] ];
array[1..numcourses] of float:
all_units = [ [[allunits]] ];
%% 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 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 from (.*)$', rule)
if m1:
crs = m1.group(1).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)
# 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:
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)
# 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']
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')
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 ('')

View File

@ -19,21 +19,6 @@ def num(s):
except ValueError:
return float(s)
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)
class CourseOr:

View File

@ -1,4 +1,4 @@
import heapq, re, csv, os, shutil, datetime, urllib
import heapq, re, csv, os, shutil, datetime, urllib, sys
import itertools, time, markdown, csv, json, os.path, webbrowser, threading
from functools import wraps
from flask import Flask, request, send_from_directory, Response, render_template
@ -6,7 +6,9 @@ from flask import send_file
from flask_socketio import SocketIO, emit
from werkzeug.routing import PathConverter
from queue import Queue
from textual.app import App, ComposeResult
from textual.containers import ScrollableContainer
from textual.widgets import Button, Footer, Header, Static
from importlib import reload
import server
@ -355,9 +357,48 @@ def serve():
y.start()
if __name__ == '__main__':
serve()
class CanvasApp(App):
"""A Textual app to manage canvas."""
BINDINGS = [("d", "toggle_dark", "Toggle dark mode")]
def compose(self) -> ComposeResult:
"""Create child widgets for the app."""
yield Header()
yield Footer()
def action_toggle_dark(self) -> None:
"""An action to toggle dark mode."""
self.dark = not self.dark
def text_app():
app = CanvasApp()
app.run()
if __name__ == "__main__":
options = { 1: ['start web server',serve] ,
2: ['start text app', text_app],
}
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]()

View File

@ -75,6 +75,7 @@ student_orientation_participation = f'cache/participation_orientation_courses.js
def num(s):
if s == '': return 0
s = re.sub(r'\.0','',s)
try:
return int(s)
except ValueError:
@ -487,6 +488,8 @@ def reorganize_grades_student():
output_s.write("student,courses\n")
output = csv.writer(output_f)
output.writerow("course_code course pocr_status orientation_status teacher_code mode student_id scaled_score".split(" "))
# student id 0 has no courses
output.writerow([0,])
for st in students:
courses = [r[1] for r in bystudent[st]]
scores = [r[7] for r in bystudent[st]]
@ -558,6 +561,7 @@ def shell2course(shell):
def stu_record_line(line):
line = line.strip()
line = line.strip(',')
parts = line.split(',')
stu_id = parts[0]
courses = []
@ -565,40 +569,50 @@ def stu_record_line(line):
courses.append(C.split('|'))
return stu_id, courses
def stu_record_to_vector(line):
def stu_record_to_vector(line, boolean=0):
id, courses = stu_record_line(line)
yesval = "true" if boolean else 1
noval = "false" if boolean else 0
template = json.loads(codecs.open('cache/courses/course_main_record.json','r','utf-8').read())
lookup = {}
for i,c in enumerate(template):
lookup[c] = i
vector = [0 for x in range(len(template))]
vector = [noval for x in range(len(template))]
for C in courses:
goodname = shell2course(C[0])
if goodname:
vector[lookup[goodname]] = 1 # C[1] # score
return id,vector
vector[lookup[goodname]] = yesval # C[1] # score
return id,vector,courses
def grades_to_vectors(verbose=1):
def grades_to_vectors(boolean=0, verbose=0):
grades = codecs.open('cache/courses_student_scores.csv','r','utf-8').readlines()
for L in grades:
id, vector = stu_record_to_vector(L)
id, vector, courses = stu_record_to_vector(L,boolean)
if verbose: print(id, vector)
yield id, vector, courses
def course_main_record():
return json.loads(codecs.open('cache/courses/course_main_record.json','r','utf-8').read())
def courses_to_vector(course_list):
def courses_to_vector(course_list, boolean=1):
#print(course_list)
yesval = "true" if boolean else 1
noval = "false" if boolean else 0
template = course_main_record()
lookup = {}
for i,c in enumerate(template):
lookup[c] = i
vector = [0 for x in range(len(template))]
vector = [noval for x in range(len(template))]
for C in course_list:
goodname = shell2course(C[0])
if goodname:
vector[lookup[goodname]] = 1 # C[1] # score
C = C.strip()
#goodname = shell2course(C[0])
#if goodname:
#print(C)
vector[lookup[C]] = yesval # C[1] # score
#print(vector)
return vector
def course_vector_to_names(vector):
@ -611,22 +625,35 @@ def course_vector_to_names(vector):
def all_course_names():
complete_list = {}
missing_names = {}
with open(student_courses_scores,'r') as input_f:
for L in input_f:
stu_id, courses = stu_record_line(L)
for C in courses:
real_name = shell2course(C[0])
if real_name:
complete_list[real_name] = 1
else:
missing_names[C[0]] = 1
master_record = sorted(complete_list.keys())
print(f"Found {len(master_record)} courses")
print(master_record)
print(f"Missing {len(missing_names)} courses")
print(missing_names)
ac = json.loads(codecs.open('cache/courses/courses_built.json','r','utf-8').read())
master_record = []
for C in ac.values():
if C['status'] == 'Draft':
continue
name = C['dept'] + C['number']
master_record.append(name)
master_record = set(master_record)
master_record = list(master_record)
master_record = sorted(master_record)
## Extract from all 'accomplished courses'...
if 0:
complete_list = {}
missing_names = {}
with open(student_courses_scores,'r') as input_f:
for L in input_f:
stu_id, courses = stu_record_line(L)
for C in courses:
real_name = shell2course(C[0])
if real_name:
complete_list[real_name] = 1
else:
missing_names[C[0]] = 1
master_record = sorted(complete_list.keys())
print(f"Found {len(master_record)} courses")
print(master_record)
print(f"Missing {len(missing_names)} courses")
print(missing_names)
mr = codecs.open('cache/courses/course_main_record.json','w','utf-8')
mr.write(json.dumps(master_record,indent=2))