From c65f430d62d0e663fc0992def70ec1db8d81dc0c Mon Sep 17 00:00:00 2001 From: Hayden Free Date: Mon, 10 Jun 2024 14:15:01 -0400 Subject: [PATCH] remove .cov files --- src/CSVUtilities.jl.33665.cov | 352 -------- src/CurricularAnalytics.jl.33665.cov | 797 ------------------ src/DataHandler.jl.33665.cov | 406 --------- src/DataTypes/Course.jl.33665.cov | 204 ----- src/DataTypes/CourseCatalog.jl.33665.cov | 56 -- src/DataTypes/Curriculum.jl.33665.cov | 396 --------- src/DataTypes/DegreePlan.jl.33665.cov | 226 ----- src/DataTypes/LearningOutcome.jl.33665.cov | 64 -- src/DataTypes/Requirements.jl.33665.cov | 320 ------- src/DataTypes/Simulation.jl.33665.cov | 50 -- src/DataTypes/Student.jl.33665.cov | 41 - src/DataTypes/StudentRecord.jl.33665.cov | 37 - .../TransferArticulation.jl.33665.cov | 42 - src/DegreePlanAnalytics.jl.33665.cov | 141 ---- src/DegreePlanCreation.jl.33665.cov | 84 -- src/GraphAlgs.jl.33665.cov | 381 --------- src/RequirementsAnalytics.jl.33665.cov | 157 ---- src/Simulation/Enrollment.jl.33665.cov | 155 ---- src/Simulation/PassRate.jl.33665.cov | 147 ---- src/Simulation/Simulation.jl.33665.cov | 238 ------ test/DataHandler.jl.33665.cov | 305 ------- test/DegreePlanCreation.jl.33665.cov | 75 -- 22 files changed, 4674 deletions(-) delete mode 100644 src/CSVUtilities.jl.33665.cov delete mode 100644 src/CurricularAnalytics.jl.33665.cov delete mode 100644 src/DataHandler.jl.33665.cov delete mode 100644 src/DataTypes/Course.jl.33665.cov delete mode 100644 src/DataTypes/CourseCatalog.jl.33665.cov delete mode 100644 src/DataTypes/Curriculum.jl.33665.cov delete mode 100644 src/DataTypes/DegreePlan.jl.33665.cov delete mode 100644 src/DataTypes/LearningOutcome.jl.33665.cov delete mode 100644 src/DataTypes/Requirements.jl.33665.cov delete mode 100644 src/DataTypes/Simulation.jl.33665.cov delete mode 100644 src/DataTypes/Student.jl.33665.cov delete mode 100644 src/DataTypes/StudentRecord.jl.33665.cov delete mode 100644 src/DataTypes/TransferArticulation.jl.33665.cov delete mode 100644 src/DegreePlanAnalytics.jl.33665.cov delete mode 100644 src/DegreePlanCreation.jl.33665.cov delete mode 100644 src/GraphAlgs.jl.33665.cov delete mode 100644 src/RequirementsAnalytics.jl.33665.cov delete mode 100644 src/Simulation/Enrollment.jl.33665.cov delete mode 100644 src/Simulation/PassRate.jl.33665.cov delete mode 100644 src/Simulation/Simulation.jl.33665.cov delete mode 100644 test/DataHandler.jl.33665.cov delete mode 100644 test/DegreePlanCreation.jl.33665.cov diff --git a/src/CSVUtilities.jl.33665.cov b/src/CSVUtilities.jl.33665.cov deleted file mode 100644 index f9287eb..0000000 --- a/src/CSVUtilities.jl.33665.cov +++ /dev/null @@ -1,352 +0,0 @@ - 6 function readfile(file_path) - 6 open(file_path) do f - 6 lines = readlines(f) - 6 return lines - - end - - end - - - 6 function remove_empty_lines(file_path) - 12 if file_path[end-3:end] != ".csv" - 0 println("Input is not a csv file") - 0 return false - - end - 12 temp_file = file_path[1:end-4] * "_temp.csv" - 6 file = readfile(file_path) - 6 open(temp_file, "w") do f - 6 new_file = "" - 6 for line in file - 149 line = replace(line, "\r" => "") - 298 if length(line) > 0 && !startswith(replace(line,"\""=>""), "#") - 149 line = line * "\n" - 155 new_file = new_file * line - - end - - end - 6 if length(new_file) > 0 - 6 new_file = chop(new_file) - - end - 6 write(f,new_file) - - end - 6 return temp_file - - end - - - - function find_courses(courses, course_id) - - for course in courses - - if course_id == course.id - - return true - - end - - end - - return false - - end - - - 24 function course_line(course, term_id; metrics=false) - 24 course_ID = course.id - 12 course_name = course.name - 12 course_prefix = course.prefix - 12 course_num = course.num - 12 course_vertex = course.vertex_id - 12 course_prereq = "\"" - 12 course_coreq = "\"" - 12 course_scoreq = "\"" - 18 for requesite in course.requisites - 10 if requesite[2] == pre - 8 course_prereq = course_prereq * string(requesite[1]) * ";" - 2 elseif requesite[2] == co - 2 course_coreq = course_coreq * string(requesite[1]) * ";" - 0 elseif requesite[2] == strict_co - 14 course_scoreq = course_scoreq * string(requesite[1]) * ";" - - end - - end - 12 course_prereq = chop(course_prereq) - 12 if length(course_prereq) > 0 - 6 course_prereq = course_prereq * "\"" - - end - 12 course_coreq = chop(course_coreq) - 12 if length(course_coreq) > 0 - 2 course_coreq = course_coreq * "\"" - - end - 12 course_scoreq = chop(course_scoreq) - 12 if length(course_scoreq) > 0 - 0 course_scoreq = course_scoreq * "\"" - - end - 12 course_chours = course.credit_hours - 12 course_inst = course.institution - 12 course_canName = course.canonical_name - 12 course_term = typeof(term_id) == Int ? string(term_id) : term_id - 18 course_term = course_term == "" ? "" : course_term * "," - 12 if metrics == false - 12 c_line= "\n" * string(course_ID) * ",\"" * string(course_name) * "\",\"" * string(course_prefix) * "\",\"" * - - string(course_num) * "\"," * string(course_prereq) * "," * string(course_coreq) * "," * - - string(course_scoreq) * "," * string(course_chours) *",\""* string(course_inst) * "\",\"" * - - string(course_canName) * "\"," * course_term - - else - - # protect against missing metrics values in course - 0 if !haskey(course.metrics, "complexity") || !haskey(course.metrics, "blocking factor") || !haskey(course.metrics, "delay factor") || !haskey(course.metrics, "centrality") - 0 error("Cannot call course_line(;metrics=true) if the curriculum's courses do not have complexity, blocking factor, delay factor, and centrality values stored in their metrics dictionary.") - - end - 0 complexity = course.metrics["complexity"] - 0 blocking_factor = course.metrics["blocking factor"] - 0 delay_factor = course.metrics["delay factor"] - 0 centrality = course.metrics["centrality"] - 0 c_line= "\n" * string(course_ID) * ",\"" * string(course_name) * "\",\"" * string(course_prefix) * "\",\"" * - - string(course_num) * "\"," * string(course_prereq) * "," * string(course_coreq) * "," * - - string(course_scoreq) * "," * string(course_chours) *",\""* string(course_inst) * "\",\"" * - - string(course_canName) * "\"," * course_term * string(complexity) * "," * string(blocking_factor) * "," * - - string(delay_factor) * "," * string(centrality) - - end - 12 return c_line - - end - - - 155 function csv_line_reader(line::AbstractString, delimeter::Char=',') - 155 quotes = false - 155 result = String[] - 155 item = "" - 304 for ch in line - 9134 if ch == '"' - 156 quotes != quotes - 8978 elseif ch == delimeter && !quotes - 1457 push!(result,item) - 1457 item = "" - - else - 25640 item = item * string(ch) - - end - - end - 155 if length(item) > 0 - 92 push!(result, item) - - end - - # check if the bounds o - 161 if isassigned(result, 1) - 149 if occursin("\ufeff", result[1]) - 0 result[1] = replace(result[1], "\ufeff" => "") - - end - - end - 155 return result - - end - - - 868 function find_cell(row, header) - 5002 if !(string(header) in names(row)) #I assume this means if header is not in names - 0 error("$(header) column is missing") - 1736 elseif typeof(row[header]) == Missing - 329 return "" - - else - 539 return row[header] - - end - - end - - - 6 function read_all_courses(df_courses::DataFrame, lo_Course:: Dict{Int, Array{LearningOutcome}}=Dict{Int, Array{LearningOutcome}}()) - 6 course_dict = Dict{Int, Course}() - 12 for row in DataFrames.eachrow(df_courses) - 206 c_ID = row[Symbol("Course ID")] - 103 c_Name = find_cell(row, Symbol("Course Name")) - 206 c_Credit = row[Symbol("Credit Hours")] - 103 c_Credit = typeof(c_Credit) == String ? parse(Float64, c_Credit) : c_Credit - 103 c_Prefix = string(find_cell(row, Symbol("Prefix"))) - 103 c_Number = find_cell(row, Symbol("Number")) - 103 if typeof(c_Number) != String - 0 c_Number = string(c_Number) - - end - 103 c_Inst = "" - 103 c_col_name = "" - 103 try - 103 c_Inst = find_cell(row, Symbol("Institution")) - 103 c_col_name = find_cell(row, Symbol("Canonical Name")) - - catch - 0 nothing - - end - - - 309 learning_outcomes = if c_ID in keys(lo_Course) lo_Course[c_ID] else LearningOutcome[] end - 103 if c_ID in keys(course_dict) - 0 println("Course IDs must be unique") - 0 return false - - else - 200 course_dict[c_ID] = Course(c_Name, c_Credit, prefix=c_Prefix, learning_outcomes=learning_outcomes, - - num=c_Number, institution=c_Inst, canonical_name=c_col_name, id=c_ID) - - end - - end - 12 for row in DataFrames.eachrow(df_courses) - 206 c_ID = row[Symbol("Course ID")] - 103 pre_reqs = find_cell(row, Symbol("Prerequisites")) - 103 if pre_reqs != "" && pre_reqs != " " && pre_reqs != ' ' - 96 if last(pre_reqs, 1) == ";" || last(pre_reqs, 1) == " " - 0 pre_reqs = pre_reqs[1:end-1] - - end - - # check if pre_reqs string contains a comma - 48 if occursin(",", string(pre_reqs)) - 0 split_prereqs = split(string(pre_reqs), ",") - - else - 48 split_prereqs = split(string(pre_reqs), ";") - - end - 48 for pre_req in split_prereqs - 117 add_requisite!(course_dict[parse(Int, pre_req)], course_dict[c_ID], pre) - - end - - end - 103 co_reqs = find_cell(row, Symbol("Corequisites")) - 103 if co_reqs != "" && co_reqs != " " && co_reqs != ' ' - 34 if last(co_reqs, 1) == ";" || last(co_reqs, 1) == " " - 0 co_reqs = co_reqs[1:end-1] - - end - - # check if co_reqs string contains a comma - 17 if occursin(",", string(co_reqs)) - 0 split_coreqs = split(string(co_reqs), ",") - - else - 17 split_coreqs = split(string(co_reqs), ";") - - end - 17 for co_req in split_coreqs - 35 add_requisite!(course_dict[parse(Int, co_req)], course_dict[c_ID], co) - - end - - end - 103 sco_reqs = find_cell(row, Symbol("Strict-Corequisites")) - 103 if sco_reqs != "" && sco_reqs != " " && sco_reqs != ' ' - 12 if last(sco_reqs) == ";" - 0 chop(sco_reqs, tail=1) - - end - - # check if sco_reqs string contains a comma - 12 if occursin(",", string(sco_reqs)) - 0 split_scoreqs = split(string(sco_reqs), ",") - - else - 12 split_scoreqs = split(string(sco_reqs), ";") - - end - 12 for sco_req in split_scoreqs - 110 add_requisite!(course_dict[parse(Int, sco_req)], course_dict[c_ID], strict_co) - - end - - end - - - - end - 6 return course_dict - - end - - - 2 function read_courses(df_courses::DataFrame, all_courses::Dict{Int,Course}) - 2 course_dict = Dict{Int, Course}() - 3 for row in DataFrames.eachrow(df_courses) - 8 c_ID = row[Symbol("Course ID")] - 7 course_dict[c_ID] = all_courses[c_ID] - - end - 2 return course_dict - - end - - - 2 function read_terms(df_courses::DataFrame, course_dict::Dict{Int, Course}, course_arr::Array{Course,1}) - 2 terms = Dict{Int, Array{Course}}() - 2 have_term = Course[] - 2 not_have_term = Course[] - 4 for row in DataFrames.eachrow(df_courses) - 22 c_ID = find_cell(row, Symbol("Course ID")) - 22 term_ID = find_cell(row, Symbol("Term")) - 22 for course in course_arr - 157 if course_dict[c_ID].id == course.id #This could be simplified with logic - 44 if typeof(row[Symbol("Term")]) != Missing #operations rather than four if statemnts - 22 push!(have_term, course) - 22 if term_ID in keys(terms) - 15 push!(terms[term_ID], course) - - else - 29 terms[term_ID] = [course] - - end - - else - 0 push!(not_have_term, course) - - end - 177 break - - end - - end - - end - 2 terms_arr = Array{Term}(undef, length(terms)) - 4 for term in terms - 12 terms_arr[term[1]] = Term([class for class in term[2]]) - - end - 2 if length(not_have_term) == 0 - 2 return terms_arr - - else - 0 return terms_arr, have_term, not_have_term - - end - - end - - - 0 function generate_course_lo(df_learning_outcomes::DataFrame) - 0 lo_dict = Dict{Int, LearningOutcome}() - 0 for row in DataFrames.eachrow(df_learning_outcomes) - 0 lo_ID = find_cell(row, Symbol("Learning Outcome ID")) - 0 lo_name = find_cell(row, Symbol("Learning Outcome")) - 0 lo_description = find_cell(row, Symbol("Description")) - 0 lo_Credit = find_cell(row, Symbol("Hours")) - 0 if lo_ID in keys(lo_dict) - 0 println("Learning Outcome ID must be unique") - 0 return false - - else - 0 lo_dict[lo_ID] = LearningOutcome(lo_name, lo_description, lo_Credit) - - end - - end - 0 for row in DataFrames.eachrow(df_learning_outcomes) - 0 lo_ID = find_cell(row, Symbol("Learning Outcome ID")) - 0 reqs = find_cell(row, Symbol("Requisites")) - 0 if typeof(reqs) != Missing - 0 for req in req - - # adds all requisite courses for the learning outcome as prerequisites - 0 add_lo_requisite!(lo_dict[req], lo_dict[lo_ID], pre) - - end - - end - - end - 0 lo_Course = Dict{Int, Array{LearningOutcome}}() - 0 for row in DataFrames.eachrow(df_learning_outcomes) - 0 c_ID = find_cell(row, Symbol("Course ID")) - 0 lo_ID = find_cell(row, Symbol("Learning Outcome ID")) - 0 if c_ID in keys(lo_Course) - 0 push!(lo_Course[c_ID], lo_dict[lo_ID]) - - else - 0 lo_Course[c_ID] = [lo_dict[lo_ID]] - - end - - end - 0 return lo_Course - - end - - - 0 function generate_curric_lo(df_curric_lo::DataFrame) - 0 learning_outcomes = LearningOutcome[] - 0 for row in DataFrames.eachrow(df_curric_lo) - 0 lo_name = find_cell(row, Symbol("Learning Outcome")) - 0 lo_description = find_cell(row, Symbol("Description")) - 0 push!(learning_outcomes, LearningOutcome(lo_name, lo_description, 0)) - - end - 0 return learning_outcomes - - end - - - - function gather_learning_outcomes(curric::Curriculum) - - all_course_lo = Dict{Int,Array{LearningOutcome,1}}() - - for course in curric.courses - - if length(course.learning_outcomes)>0 - - all_course_lo[course.id] = course.learning_outcomes - - end - - end - - return all_course_lo - - end - - - 2 function write_learning_outcomes(curric::Curriculum, csv_file, all_course_lo) - 2 if length(all_course_lo) > 0 - 0 write(csv_file, "\nCourse Learning Outcomes,,,,,,,,,,") - 0 write(csv_file, "\nCourse ID,Learning Outcome ID,Learning Outcome,Description,Requisites,Hours,,,,,") - 0 for lo_arr in all_course_lo - 0 for lo in lo_arr[2] - 0 course_ID = lo_arr[1] - 0 lo_ID = lo.id - 0 lo_name = lo.name - 0 lo_desc = lo.description - 0 lo_prereq = "\"" - 0 for requesite in lo.requisites - 0 lo_prereq = lo_prereq*string(requesite[1]) * ";" - - end - 0 lo_prereq = chop(lo_prereq) - 0 if length(lo_prereq) > 0 - 0 lo_prereq = lo_prereq * "\"" - - end - 0 lo_hours = lo.hours - 0 lo_line = "\n" * string(course_ID) * "," * string(lo_ID) * ",\"" * string(lo_name) * "\",\"" * - - string(lo_desc) * "\",\"" * string(lo_prereq) * "\"," * string(lo_hours) * ",,,,," - 0 write(csv_file, lo_line) - - end - - end - - end - 2 if length(curric.learning_outcomes) > 0 - 0 write(csv_file, "\nCurriculum Learning Outcomes,,,,,,,,,,") - 0 write(csv_file, "\nLearning Outcome,Description,,,,,,,,,") - 0 for lo in curric.learning_outcomes - 0 lo_name = lo.name - 0 lo_desc = lo.description - 0 lo_line = "\n\"" * string(lo_name) *"\",\""* string(lo_desc) * "\",,,,,,,,," - 2 write(csv_file, lo_line) - - end - - end - - end diff --git a/src/CurricularAnalytics.jl.33665.cov b/src/CurricularAnalytics.jl.33665.cov deleted file mode 100644 index e944ff7..0000000 --- a/src/CurricularAnalytics.jl.33665.cov +++ /dev/null @@ -1,797 +0,0 @@ - - """ - - The curriculum-based metrics in this toolbox are based upon the graph structure of a - - curriculum. Specifically, assume curriculum ``c`` consists of ``n`` courses ``\\{c_1, \\ldots, c_n\\}``, - - and that there are ``m`` requisite (prerequisite or co-requsitie) relationships between these courses. - - A curriculum graph ``G_c = (V,E)`` is formed by creating a vertex set ``V = \\{v_1, \\ldots, v_n\\}`` - - (i.e., one vertex for each course) along with an edge set ``E = \\{e_1, \\ldots, e_m\\}``, where a - - directed edge from vertex ``v_i`` to ``v_j`` is in ``E`` if course ``c_i`` is a requisite for course ``c_j``. - - """ - - module CurricularAnalytics - - - - # Dependencies - - using Graphs - - using DataStructures - - using Printf - - using Markdown - - using Documenter - - using Dates - - using MetaGraphs - - using Combinatorics - - - - include("DataTypes/DataTypes.jl") - - include("DataHandler.jl") - - include("GraphAlgs.jl") - - include("DegreePlanAnalytics.jl") - - include("DegreePlanCreation.jl") - - include("Simulation/Simulation.jl") - - include("RequirementsAnalytics.jl") - - - - export AA, AAS, AS, AbstractCourse, AbstractRequirement, BA, BS, Course, CourseCollection, CourseCatalog, CourseRecord, CourseSet, Curriculum, DegreePlan, - - EdgeClass, Enrollment, Grade, LearningOutcome, PassRate, RequirementSet, Requisite, Student, StudentRecord, Simulation, System, Term, TransferArticulation, - - add_course!, add_lo_requisite!, add_requisite!, add_transfer_catalog, add_transfer_course, all_paths, back_edge, basic_metrics, basic_statistics, - - bin_filling, blocking_factor, centrality, co, compare_curricula, convert_ids, complexity, course, course_from_id, course_from_vertex, course_id, - - courses_from_vertices, create_degree_plan, cross_edge, dead_ends, delay_factor, delete_requisite!, dfs, extraneous_requisites, find_term, forward_edge, - - gad, grade, homology, is_duplicate, is_valid, isvalid_curriculum, isvalid_degree_plan, level, longest_path, longest_paths, merge_curricula, pass_table, passrate_table, - - pre, postorder_traversal, preorder_traversal, print_plan, quarter, reach, reach_subgraph, reachable_from, reachable_from_subgraph, reachable_to, reachable_to_subgraph, - - read_csv, requisite_distance,requisite_type, semester, set_passrates, set_passrate_for_course, set_passrates_from_csv, show_requirements, similarity, simple_students, - - simulate, simulation_report, strict_co, topological_sort, total_credits, transfer_equiv, tree_edge, write_csv, knowledge_transfer, csv_stream - - - - """ - - extraneous_requisites(c::Curriculum; print=false) - - - - Determines whether or not a curriculum `c` contains extraneous requisites, and returns them. Extraneous requisites - - are redundant requisites that are unnecessary in a curriculum. For example, if a curriculum has the prerequisite - - relationships \$c_1 \\rightarrow c_2 \\rightarrow c_3\$ and \$c_1 \\rightarrow c_3\$, and \$c_1\$ and \$c_2\$ are - - *not* co-requisites, then \$c_1 \\rightarrow c_3\$ is redundant and therefore extraneous. - - """ - 8 function extraneous_requisites(c::Curriculum; print=false) - 8 if is_cyclic(c.graph) - 0 error("\nCurriculm graph has cycles, extraneous requisities cannot be determined.") - - end - 4 if print == true - 0 msg = IOBuffer() - - end - 4 redundant_reqs = Array{Array{Int,1},1}() - 4 g = c.graph - 4 que = Queue{Int}() - 4 components = weakly_connected_components(g) - 4 extraneous = false - 4 str = "" # create an empty string to hold messages - 4 for wcc in components - 8 if length(wcc) > 1 # only consider components with more than one vertex - 4 for u in wcc - 23 nb = neighbors(g,u) - 30 for n in nb - 38 enqueue!(que, n) - - end - 68 while !isempty(que) - 45 x = dequeue!(que) - 45 nnb = neighbors(g,x) - 72 for n in nnb - 41 enqueue!(que, n) - - end - 72 for v in neighbors(g, x) - 40 if has_edge(g, u, v) # possible redundant requsisite - - # TODO: If this edge is a co-requisite it is an error, as it would be impossible to satsify. - - # This needs to be checked here. - 2 remove = true - 2 for n in nb # check for co- or strict_co requisites - 4 if has_path(c.graph, n, v) # is there a path from n to v? - 4 req_type = c.courses[n].requisites[c.courses[u].id] # the requisite relationship between u and n - 7 if (req_type == co) || (req_type == strict_co) # is u a co or strict_co requisite for n? - 3 remove = false # a co or strict_co relationshipo is involved, must keep (u, v) - - end - - end - - end - 2 if remove == true - 1 if findfirst(x -> x == [c.courses[u].id, c.courses[v].id], redundant_reqs) == nothing # make sure redundant requisite wasn't previously found - 1 push!(redundant_reqs, [c.courses[u].id, c.courses[v].id]) - 1 if print == true - 0 str = str * "-$(c.courses[v].name) has redundant requisite $(c.courses[u].name)\n" - - end - - end - 28 extraneous = true - - end - - end - - end - - end - - end - - end - - end - 4 if (extraneous == true) && (print == true) - 0 c.institution != "" ? write(msg, "\n$(c.institution): ") : "\n" - 0 write(msg, "curriculum $(c.name) has extraneous requisites:\n") - 0 write(msg, str) - - end - 4 if print == true - 0 println(String(take!(msg))) - - end - 4 return redundant_reqs - - end - - - - # Compute the blocking factor of a course - - """ - - blocking_factor(c::Curriculum, course::Int) - - - - The **blocking factor** associated with course ``c_i`` in curriculum ``c`` with - - curriculum graph ``G_c = (V,E)`` is defined as: - - ```math - - b_c(v_i) = \\sum_{v_j \\in V} I(v_i,v_j) - - ``` - - where ``I(v_i,v_j)`` is the indicator function, which is ``1`` if ``v_i \\leadsto v_j``, - - and ``0`` otherwise. Here ``v_i \\leadsto v_j`` denotes that a directed path from vertex - - ``v_i`` to ``v_j`` exists in ``G_c``, i.e., there is a requisite pathway from course - - ``c_i`` to ``c_j`` in curriculum ``c``. - - """ - 23 function blocking_factor(c::Curriculum, course::Int) - 23 b = length(reachable_from(c.graph, course)) - 23 return c.courses[course].metrics["blocking factor"] = b - - end - - - - # Compute the blocking factor of a curriculum - - """ - - blocking_factor(c::Curriculum) - - - - The **blocking factor** associated with curriculum ``c`` is defined as: - - ```math - - b(G_c) = \\sum_{v_i \\in V} b_c(v_i). - - ``` - - where ``G_c = (V,E)`` is the curriculum graph associated with curriculum ``c``. - - """ - 3 function blocking_factor(c::Curriculum) - 3 b = 0 - 3 bf = Array{Int, 1}(undef, c.num_courses) - 6 for (i, v) in enumerate(vertices(c.graph)) - 23 bf[i] = blocking_factor(c, v) - 43 b += bf[i] - - end - 3 return c.metrics["blocking factor"] = b, bf - - end - - - - # Compute the delay factor of a course - - """ - - delay_factor(c::Curriculum, course::Int) - - - - The **delay factor** associated with course ``c_k`` in curriculum ``c`` with - - curriculum graph ``G_c = (V,E)`` is the number of vertices in the longest path - - in ``G_c`` that passes through ``v_k``. If ``\\#(p)`` denotes the number of - - vertices in the directed path ``p`` in ``G_c``, then we can define the delay factor of - - course ``c_k`` as: - - ```math - - d_c(v_k) = \\max_{i,j,l,m}\\left\\{\\#(v_i \\overset{p_l}{\\leadsto} v_k \\overset{p_m}{\\leadsto} v_j)\\right\\} - - ``` - - where ``v_i \\overset{p}{\\leadsto} v_j`` denotes a directed path ``p`` in ``G_c`` from vertex - - ``v_i`` to ``v_j``. - - """ - - function delay_factor(c::Curriculum, course::Int) - - if !haskey(c.courses[course].metrics, "delay factor") - - delay_factor(c) - - end - - return c.courses[course].metrics["delay factor"] - - end - - - - # Compute the delay factor of a curriculum - - """ - - delay_factor(c::Curriculum) - - - - The **delay factor** associated with curriculum ``c`` is defined as: - - ```math - - d(G_c) = \\sum_{v_k \\in V} d_c(v_k). - - ``` - - where ``G_c = (V,E)`` is the curriculum graph associated with curriculum ``c``. - - """ - 4 function delay_factor(c::Curriculum) - 4 g = c.graph - 27 df = ones(c.num_courses) - 8 for v in vertices(g) - 27 for path in all_paths(g) - 107 for vtx in path - 347 path_length = length(path) # path_length in terms of # of vertices, not edges - 347 if path_length > df[vtx] - 53 df[vtx] = path_length - - end - - end - - end - - end - 4 d = 0 - 4 c.metrics["delay factor"] = 0 - 8 for v in vertices(g) - 27 c.courses[v].metrics["delay factor"] = df[v] - 50 d += df[v] - - end - 4 return c.metrics["delay factor"] = d, df - - end - - - - # Compute the centrality of a course - - """ - - centrality(c::Curriculum, course::Int) - - - - Consider a curriculum graph ``G_c = (V,E)``, and a vertex ``v_i \\in V``. Furthermore, - - consider all paths between every pair of vertices ``v_j, v_k \\in V`` that satisfy the - - following conditions: - - - ``v_i, v_j, v_k`` are distinct, i.e., ``v_i \\neq v_j, v_i \\neq v_k`` and ``v_j \\neq v_k``; - - - there is a path from ``v_j`` to ``v_k`` that includes ``v_i``, i.e., ``v_j \\leadsto v_i \\leadsto v_k``; - - - ``v_j`` has in-degree zero, i.e., ``v_j`` is a "source"; and - - - ``v_k`` has out-degree zero, i.e., ``v_k`` is a "sink". - - Let ``P_{v_i} = \\{p_1, p_2, \\ldots\\}`` denote the set of all directed paths that satisfy these - - conditions. - - Then the **centrality** of ``v_i`` is defined as - - ```math - - q(v_i) = \\sum_{l=1}^{\\left| P_{v_i} \\right|} \\#(p_l). - - ``` - - where ``\\#(p)`` denotes the number of vertices in the directed path ``p`` in ``G_c``. - - """ - 23 function centrality(c::Curriculum, course::Int) - 46 cent = 0; g = c.graph - 23 for path in all_paths(g) - - # conditions: path length is greater than 2, target course must be in the path, the target vertex - - # cannot be the first or last vertex in the path - 99 if (in(course,path) && length(path) > 2 && path[1] != course && path[end] != course) - 40 cent += length(path) - - end - - end - 23 return c.courses[course].metrics["centrality"] = cent - - end - - - - # Compute the total centrality of all courses in a curriculum - - """ - - centrality(c::Curriculum) - - - - Computes the total **centrality** associated with all of the courses in curriculum ``c``, - - with curriculum graph ``G_c = (V,E)``. - - ```math - - q(c) = \\sum_{v \\in V} q(v). - - ``` - - """ - 3 function centrality(c::Curriculum) - 3 cent = 0 - 3 cf = Array{Int, 1}(undef, c.num_courses) - 6 for (i, v) in enumerate(vertices(c.graph)) - 23 cf[i] = centrality(c, v) - 43 cent += cf[i] - - end - 3 return c.metrics["centrality"] = cent, cf - - end - - - - # Compute the complexity of a course - - """ - - complexity(c::Curriculum, course::Int) - - - - The **complexity** associated with course ``c_i`` in curriculum ``c`` with - - curriculum graph ``G_c = (V,E)`` is defined as: - - ```math - - h_c(v_i) = d_c(v_i) + b_c(v_i) - - ``` - - i.e., as a linear combination of the course delay and blocking factors. - - """ - - function complexity(c::Curriculum, course::Int) - - if !haskey(c.courses[course].metrics, "complexity") - - complexity(c) - - end - - return c.courses[course].metrics["complexity"] - - end - - - - # Compute the complexity of a curriculum - - """ - - complexity(c::Curriculum, course::Int) - - - - The **complexity** associated with curriculum ``c`` with curriculum graph ``G_c = (V,E)`` - - is defined as: - - - - ```math - - h(G_c) = \\sum_{v \\in V} \\left(d_c(v) + b_c(v)\\right). - - ``` - - - - For the example curricula considered above, the curriculum in part (a) has an overall complexity of 15, - - while the curriculum in part (b) has an overall complexity of 17. This indicates that the curriculum - - in part (b) will be slightly more difficult to complete than the one in part (a). In particular, notice - - that course ``v_1`` in part (a) has the highest individual course complexity, but the combination of - - courses ``v_1`` and ``v_2`` in part (b), which both must be passed before a student can attempt course - - ``v_3`` in that curriculum, has a higher combined complexity. - - """ - 3 function complexity(c::Curriculum) - 3 course_complexity = Array{Number, 1}(undef, c.num_courses) - 3 curric_complexity = 0 - 3 if !haskey(c.metrics, "delay factor") - 1 delay_factor(c) - - end - 3 if !haskey(c.metrics, "blocking factor") - 1 blocking_factor(c) - - end - 6 for v in vertices(c.graph) - 23 c.courses[v].metrics["complexity"] = c.courses[v].metrics["delay factor"] + c.courses[v].metrics["blocking factor"] - 23 if c.system_type == quarter - 0 c.courses[v].metrics["complexity"] = round((c.courses[v].metrics["complexity"] * 2)/3, digits=1) - - end - 23 course_complexity[v] = c.courses[v].metrics["complexity"] - 43 curric_complexity += course_complexity[v] - - end - 3 return c.metrics["complexity"] = curric_complexity, course_complexity - - end - - - - # Find all the longest paths in a curriculum. - - """ - - longest_paths(c::Curriculum) - - - - Finds longest paths in curriculum `c`, and returns an array of course arrays, where - - each course array contains the courses in a longest path. - - - - # Arguments - - Required: - - - `c::Curriculum` : a valid curriculum. - - - - ```julia-repl - - julia> paths = longest_paths(c) - - ``` - - """ - 1 function longest_paths(c::Curriculum) - 1 lps = Array{Array{Course,1},1}() - 1 for path in longest_paths(c.graph) # longest_paths(), GraphAlgs.jl - 3 c_path = courses_from_vertices(c, path) - 4 push!(lps, c_path) - - end - 1 return c.metrics["longest paths"] = lps - - end - - - - # Compare the metrics associated with two curricula - - # to print out the report, use: println(String(take!(report))), where report is the IOBuffer returned by this function - - function compare_curricula(c1::Curriculum, c2::Curriculum) - - report = IOBuffer() - - if collect(keys(c1.metrics)) != collect(keys(c2.metrics)) - - error("cannot compare curricula, they do not have the same metrics") - - end - - write(report, "Comparing: C1 = $(c1.name) and C2 = $(c2.name)\n") - - for k in keys(c1.metrics) - - write(report, " Curricular $k: ") - - if length(c1.metrics[k]) == 2 # curriculum has course-level metrics - - metric1 = c1.metrics[k][1] - - metric2 = c2.metrics[k][1] - - else - - metric1 = c1.metrics[k] - - metric2 = c2.metrics[k] - - end - - diff = c1.metrics[k][1] - c2.metrics[k][1] - - if diff > 0 - - @printf(report, "C1 is %.1f units (%.0f%c) larger than C2\n", diff, 100*diff/c2.metrics[k][1], '%') - - elseif diff < 0 - - @printf(report, "C1 is %.1f units (%.0f%c) smaller than C2\n", -diff, 100*(-diff)/c2.metrics[k][1], '%') - - else - - write(report, "C1 and C2 have the same curricular $k\n") - - end - - if length(c1.metrics[k]) == 2 - - write(report, " Course-level $k:\n") - - for (i, c) in enumerate([c1, c2]) - - maxval = maximum(c.metrics[k][2]) - - pos = [j for (j, x) in enumerate(c.metrics[k][2]) if x == maxval] - - write(report, " Largest $k value in C$i is $maxval for course: ") - - for p in pos - - write(report, "$(c.courses[p].name) ") - - end - - write(report, "\n") - - end - - end - - end - - return report - - end - - - - # Create a list of courses or course names from a array of vertex IDs. - - # The array returned can be (keyword arguments): - - # -course data objects : object - - # -the names of courses : name - - # -the full names of courses (prefix, number, name) : fullname - 6 function courses_from_vertices(curriculum::Curriculum, vertices::Array{Int,1}; course::String="object") - 6 if course == "object" - 3 course_list = Course[] - - else - 0 course_list = String[] - - end - 3 for v in vertices - 9 c = curriculum.courses[v] - 9 course == "object" ? push!(course_list, c) : nothing - 9 course == "name" ? push!(course_list, "$(c.name)") : nothing - 12 course == "fullname" ? push!(course_list, "$(c.prefix) $(c.num) - $(c.name)") : nothing - - end - 3 return course_list - - end - - - - # Basic metrics for a currciulum. - - """ - - basic_metrics(c::Curriculum) - - - - Compute the basic metrics associated with curriculum `c`, and return an IO buffer containing these metrics. The basic - - metrics are also stored in the `metrics` dictionary associated with the curriculum. - - - - The basic metrics computed include: - - - - - number of credit hours : The total number of credit hours in the curriculum. - - - number of courses : The total courses in the curriculum. - - - blocking factor : The blocking factor of the entire curriculum, and of each course in the curriculum. - - - centrality : The centrality measure associated with the entire curriculum, and of each course in the curriculum. - - - delay factor : The delay factor of the entire curriculum, and of each course in the curriculum. - - - curricular complexity : The curricular complexity of the entire curriculum, and of each course in the curriculum. - - - - Complete descriptions of these metrics are provided above. - - - - ```julia-repl - - julia> metrics = basic_metrics(curriculum) - - julia> println(String(take!(metrics))) - - julia> # The metrics are also stored in a dictonary that can be accessed as follows - - julia> curriculum.metrics - - ``` - - """ - 1 function basic_metrics(curric::Curriculum) - 1 buf = IOBuffer() - 1 complexity(curric), centrality(curric), longest_paths(curric) # compute all curricular metrics - 4 max_bf = 0; max_df = 0; max_cc = 0; max_cent = 0 - 4 max_bf_courses = Array{Course,1}(); max_df_courses = Array{Course,1}(); max_cc_courses = Array{Course,1}(); max_cent_courses = Array{Course,1}() - 1 for c in curric.courses - 8 if c.metrics["blocking factor"] == max_bf - 1 push!(max_bf_courses, c) - 7 elseif c.metrics["blocking factor"] > max_bf - 2 max_bf = c.metrics["blocking factor"] - 2 max_bf_courses = Array{Course,1}() - 2 push!(max_bf_courses, c) - - end - 8 if c.metrics["delay factor"] == max_df - 4 push!(max_df_courses, c) - 4 elseif c.metrics["delay factor"] > max_df - 1 max_df = c.metrics["delay factor"] - 1 max_df_courses = Array{Course,1}() - 1 push!(max_df_courses, c) - - end - 8 if c.metrics["complexity"] == max_cc - 1 push!(max_cc_courses, c) - 7 elseif c.metrics["complexity"] > max_cc - 2 max_cc = c.metrics["complexity"] - 2 max_cc_courses = Array{Course,1}() - 2 push!(max_cc_courses, c) - - end - 8 if c.metrics["centrality"] == max_cent - 2 push!(max_cent_courses, c) - 6 elseif c.metrics["centrality"] > max_cent - 1 max_cent = c.metrics["centrality"] - 1 max_cent_courses = Array{Course,1}() - 1 push!(max_cent_courses, c) - - end - 8 curric.metrics["max. blocking factor"] = max_bf - 8 curric.metrics["max. blocking factor courses"] = max_bf_courses - 8 curric.metrics["max. centrality"] = max_cent - 8 curric.metrics["max. centrality courses"] = max_cent_courses - 8 curric.metrics["max. delay factor"] = max_df - 8 curric.metrics["max. delay factor courses"] = max_df_courses - 8 curric.metrics["max. complexity"] = max_cc - 9 curric.metrics["max. complexity courses"] = max_cc_courses - - end - - # write metrics to IO buffer - 1 write(buf, "\n$(curric.institution) ") - 1 write(buf, "\nCurriculum: $(curric.name)\n") - 1 write(buf, " credit hours = $(curric.credit_hours)\n") - 1 write(buf, " number of courses = $(curric.num_courses)") - 1 write(buf, "\n Blocking Factor --\n") - 1 write(buf, " entire curriculum = $(curric.metrics["blocking factor"][1])\n") - 1 write(buf, " max. value = $(max_bf), ") - 1 write(buf, "for course(s): ") - 1 write_course_names(buf, max_bf_courses) - 1 write(buf, "\n Centrality --\n") - 1 write(buf, " entire curriculum = $(curric.metrics["centrality"][1])\n") - 1 write(buf, " max. value = $(max_cent), ") - 1 write(buf, "for course(s): ") - 1 write_course_names(buf, max_cent_courses) - 1 write(buf, "\n Delay Factor --\n") - 1 write(buf, " entire curriculum = $(curric.metrics["delay factor"][1])\n") - 1 write(buf, " max. value = $(max_df), ") - 1 write(buf, "for course(s): ") - 1 write_course_names(buf, max_df_courses) - 1 write(buf, "\n Complexity --\n") - 1 write(buf, " entire curriculum = $(curric.metrics["complexity"][1])\n") - 1 write(buf, " max. value = $(max_cc), ") - 1 write(buf, "for course(s): ") - 1 write_course_names(buf, max_cc_courses) - 1 write(buf, "\n Longest Path(s) --\n") - 1 write(buf, " length = $(length(curric.metrics["longest paths"][1])), number of paths = $(length(curric.metrics["longest paths"]))\n path(s):\n") - 1 for (i, path) in enumerate(curric.metrics["longest paths"]) - 3 write(buf, " path $i = ") - 3 write_course_names(buf, path, separator=" -> ") - 3 write(buf, "\n") - - end - 1 return buf - - end - - - - function basic_statistics(curricula::Array{Curriculum,1}, metric_name::AbstractString) - - buf = IOBuffer() - - # set initial values used to find min and max metric values - - total_metric = 0; STD_metric = 0 - - if haskey(curricula[1].metrics, metric_name) - - if typeof(curricula[1].metrics[metric_name]) == Float64 - - max_metric = curricula[1].metrics[metric_name]; min_metric = curricula[1].metrics[metric_name]; - - elseif typeof(curricula[1].metrics[metric_name]) == Tuple{Float64,Array{Number,1}} - - max_metric = curricula[1].metrics[metric_name][1]; min_metric = curricula[1].metrics[metric_name][1]; # metric where total curricular metric as well as course-level metrics are stored in an array - - end - - end - - for c in curricula - - if !haskey(c.metrics, metric_name) - - error("metric $metric_name does not exist in curriculum $(c.name)") - - end - - basic_metrics(c) - - if typeof(c.metrics[metric_name]) == Float64 - - value = c.metrics[metric_name] - - elseif typeof(c.metrics[metric_name]) == Tuple{Float64,Array{Number,1}} - - value = c.metrics[metric_name][1] # metric where total curricular metric as well as course-level metrics are stored in an array - - end - - total_metric += value - - value > max_metric ? max_metric = value : nothing - - value < min_metric ? min_metric = value : nothing - - end - - avg_metric = total_metric / length(curricula) - - for c in curricula - - if typeof(c.metrics[metric_name]) == Float64 - - value = c.metrics[metric_name] - - elseif typeof(c.metrics[metric_name]) == Tuple{Float64,Array{Number,1}} - - value = c.metrics[metric_name][1] # metric where total curricular metric as well as course-level metrics are stored in an array - - end - - STD_metric = (value - avg_metric)^2 - - end - - STD_metric = sqrt(STD_metric / length(curricula)) - - write(buf, "\n Metric -- $metric_name") - - write(buf, "\n Number of curricula = $(length(curricula))") - - write(buf, "\n Mean = $avg_metric") - - write(buf, "\n STD = $STD_metric") - - write(buf, "\n Max. = $max_metric") - - write(buf, "\n Min. = $min_metric") - - return(buf) - - end - - - 14 function write_course_names(buf::IOBuffer, courses::Array{Course,1}; separator::String=", ") - 14 if length(courses) == 1 - 3 write_course_name(buf, courses[1]) - - else - 4 for c in courses[1:end-1] - 10 write_course_name(buf, c) - 14 write(buf, separator) - - end - 4 write_course_name(buf, courses[end]) - - end - - end - - - 17 function write_course_name(buf::IOBuffer, c::Course) - 17 !isempty(c.prefix) ? write(buf, "$(c.prefix) ") : nothing - 17 !isempty(c.num) ? write(buf, "$(c.num) - ") : nothing - 17 write(buf, "$(c.name)") # name is a required item - - end - - - - """ - - similarity(c1, c2; strict) - - - - Compute how similar curriculum `c1` is to curriculum `c2`. The similarity metric is computed by comparing how many courses in - - `c1` are also in `c2`, divided by the total number of courses in `c2`. Thus, for two curricula, this metric is not symmetric. A - - similarity value of `1` indicates that `c1` and `c2` are identical, whil a value of `0` means that none of the courses in `c1` - - are in `c2`. - - - - # Arguments - - Required: - - - `c1::Curriculum` : the target curriculum. - - - `c2::Curriculum` : the curriculum serving as the basis for comparison. - - - - Keyword: - - - `strict::Bool` : if true (default), two courses are considered the same if every field in the two courses are the same; if false, - - two courses are conisdred the same if they have the same course name, or if they have the same course prefix and number. - - - - ```julia-repl - - julia> similarity(curric1, curric2) - - ``` - - """ - 4 function similarity(c1::Curriculum, c2::Curriculum; strict::Bool=true) - 4 if c2.num_courses == 0 - 0 error("Curriculum $(c2.name) does not have any courses, similarity cannot be computed") - - end - 2 if (c1 == c2) return 1 end - 2 matches = 0 - 2 if strict == true - 2 for course in c1.courses - 15 if course in c2.courses - 16 matches += 1 - - end - - end - - else # strict == false - 0 for course in c1.courses - 0 for basis_course in c2.courses - 0 if (course.name != "" && basis_course.name == course.name) || (course.prefix != "" && basis_course.prefix == course.prefix && course.num != "" && basis_course.num == course.num) - 0 matches += 1 - 0 break # only match once - - end - - end - - end - - end - 2 return matches/c2.num_courses - - end - - - - """ - - merge_curricula(c1, c2; match_criteria) - - - - Merge the two curricula `c1` and `c2` supplied as input into a single curriculum based on the match criteria applied - - to the courses in the two curricula. All courses in curriculum `c1` will appear in the merged curriculum. If a course in - - curriculum `c2` matches a course in curriculum `c1`, that course serves as a matched course in the merged curriculum. - - If there is no match for a course in curriculum `c2` to the set of courses in curriculum `c1`, course `c2` is added - - to the set of courses in the merged curriculum. - - - - # Arguments - - Required: - - - `c1::Curriculum` : first curriculum. - - - `c2::Curriculum` : second curriculum. - - - - Optional: - - - `match_criteria::Array{String}` : list of course items that must match, if no match critera are supplied, the - - courses must be identical (at the level of memory allocation). Allowable match criteria include: - - - `prefix` : the course prefixes must be identical. - - - `num` : the course numbers must be indentical. - - - `name` : the course names must be identical. - - - `canonical name` : the course canonical names must be identical. - - - `credit hours` : the course credit hours must be indentical. - - - - """ - - function merge_curricula(name::AbstractString, c1::Curriculum, c2::Curriculum, match_criteria::Array{String}=Array{String,1}(); - - learning_outcomes::Array{LearningOutcome}=Array{LearningOutcome,1}(), degree_type::AbstractString=BS, system_type::System=semester, - - institution::AbstractString="", CIP::AbstractString="") - - merged_courses = deepcopy(c1.courses) - - extra_courses = Array{Course,1}() # courses in c2 but not in c1 - - new_courses = Array{Course,1}() - - for course in c2.courses - - matched = false - - for target_course in c1.courses - - if match(course, target_course, match_criteria) == true - - matched = true - - skip - - end - - end - - !matched ? push!(extra_courses, course) : nothing - - end - - # patch-up requisites of extra_courses, using course ids form c1 where appropriate - - for c in extra_courses - - # for each extra course create an indentical coures, but with a new course id - - push!(new_courses, Course(c.name, c.credit_hours; prefix=c.prefix, learning_outcomes=c.learning_outcomes, - - num=c.num, institution=c.institution, canonical_name=c.canonical_name)) - - end - - for (j,c) in enumerate(extra_courses) - - # print("\n $(c.name): ") - - # print("total requisistes = $(length(c.requisites)),") - - for req in keys(c.requisites) - - # print(" requisite id: $(req) ") - - req_course = course_from_id(c2, req) - - if find_match(req_course, merged_courses, match_criteria) != nothing - - # requisite already exists in c1 - - # print(" match in c1 - $(course_from_id(c1, req).name) ") - - add_requisite!(req_course, new_courses[j], c.requisites[req]) - - elseif find_match(req_course, extra_courses, match_criteria) != nothing - - # requisite is not in c1, but it's in c2 -- use the id of the new course created for it - - # print(" match in extra courses, ") - - i = findfirst(x->x==req_course, extra_courses) - - # print(" index of match = $i ") - - add_requisite!(new_courses[i], new_courses[j], c.requisites[req]) - - else # requisite is neither in c1 or 2 -- this shouldn't happen => error - - error("requisite error on course: $(c.name)") - - end - - end - - end - - merged_courses = [merged_courses; new_courses] - - merged_curric = Curriculum(name, merged_courses, learning_outcomes=learning_outcomes, degree_type=degree_type, institution=institution, CIP=CIP) - - return merged_curric - - end - - - - function match(course1::Course, course2::Course, match_criteria::Array{String}=Array{String,1}()) - - is_matched = false - - if length(match_criteria) == 0 - - return (course1 == course2) - - else - - for str in match_criteria - - if !(str in ["prefix", "num", "name", "canonical name", "credit hours"]) - - error("invalid match criteria: $str") - - elseif str == "prefix" - - course1.prefix == course2.prefix ? is_matched = true : is_matched = false - - elseif str == "num" - - course1.num == course2.num ? is_matched = true : is_matched = false - - elseif str == "name" - - course1.name == course2.name ? is_matched = true : is_matched = false - - elseif str == "canonical name" - - course1.canonical_name == course2.canonical_name ? is_matched = true : is_matched = false - - elseif str == "credit hours" - - course1.credit_hours == course2.credit_hours ? is_matched = true : is_matched = false - - end - - end - - end - - return is_matched - - end - - - - function find_match(course::Course, course_set::Array{Course}, match_criteria::Array{String}=Array{String,1}()) - - for c in course_set - - if match(course, c, match_criteria) - - return course - - end - - end - - return nothing - - end - - - - function homology(curricula::Array{Curriculum,1}; strict::Bool=false) - - similarity_matrix = Matrix{Float64}(I, length(curricula), length(curricula)) - - for i = 1:length(curricula) - - for j = 1:length(curricula) - - similarity_matrix[i,j] = similarity(curricula[i], curricula[j], strict=strict) - - similarity_matrix[j,i] = similarity(curricula[j], curricula[i], strict=strict) - - end - - end - - return similarity_matrix - - end - - - - """ - - dead_ends(curric, prefixes) - - - - Finds all courses in curriculum `curric` that appear at the end of a path (i.e., sink vertices), and returns those courses that - - do not have one of the course prefixes listed in the `prefixes` array. If a course does not have a prefix, it is excluded from - - the analysis. - - - - # Arguments - - - `c::Curriculum` : the target curriculum. - - - `prefixes::Array{String,1}` : an array of course prefix strings. - - - - For instance, the following will find all courses in `curric` that appear at the end of any course path in the curriculum, - - and do *not* have `BIO` as a prefix. One might consider these courses "dead ends," as their course outcomes are not used by any - - major-specific course, i.e., by any course with the prefix `BIO`. - - - - ```julia-repl - - julia> dead_ends(curric, ["BIO"]) - - ``` - - """ - 2 function dead_ends(curric::Curriculum, prefixes::Array{String,1}) - 2 dead_end_courses = Array{Course,1}() - 2 paths = all_paths(curric.graph) - 2 for p in paths - 9 course = course_from_vertex(curric, p[end]) - 9 if course.prefix == "" - 0 continue - - end - 18 if !(course.prefix in prefixes) - 1 if !(course in dead_end_courses) - 12 push!(dead_end_courses, course) - - end - - end - - end - 2 if haskey(curric.metrics, "dead end") - 0 if !haskey(curric.metrics["dead end"], prefixes) - 0 push!(curric.metrics["dead end"], prefixes => dead_end_courses) - - end - - else - 2 curric.metrics["dead end"] = Dict(prefixes => dead_end_courses) - - end - 2 return (prefixes, dead_end_courses) - - end - - - - """ - - knowledge_transfer(dp) - - - - Determine the number of requisites crossing the "cut" in a degree plan that occurs between each term. - - - - # Arguments - - - `dp::DegreePlan` : the degree to analyze. - - - - Returns an array of crossing values between the courses in the first term and the remainder of the degree plan, - - between the courses in the first two terms in the degree plan, and the remainder of the degree plan, etc. - - The number of values returned will be one less than the number of terms in the degree plan. - - - - ```julia-repl - - julia> knowledge_transfer(dp) - - ``` - - """ - - function knowledge_transfer(dp::DegreePlan) - - ec_terms = [] - - s = Array{Int64,1}() - - for term in dp.terms - - sum = 0 - - for c in term.courses - - push!(s, c.vertex_id[dp.curriculum.id]) - - end - - sum += edge_crossings(dp.curriculum.graph, s) - - push!(ec_terms, sum) - - end - - return deleteat!(ec_terms, lastindex(ec_terms)) # the last value will always be zero, so remove it - - end - - - - end # module diff --git a/src/DataHandler.jl.33665.cov b/src/DataHandler.jl.33665.cov deleted file mode 100644 index 366fcb2..0000000 --- a/src/DataHandler.jl.33665.cov +++ /dev/null @@ -1,406 +0,0 @@ - - # ============================== - - # CSV Read / Write Functionality - - # ============================== - - using CSV - - using DataFrames - - include("CSVUtilities.jl") - - - - """ - - read_csv(file_path::AbstractString) - - - - Read (i.e., deserialize) a CSV file containing either a curriculum or a degree plan, and returns a corresponding - - `Curriculum` or `DegreePlan` data object. The required format for curriculum or degree plan CSV files is - - described in [File Format](@ref). - - - - # Arguments - - - `file_path::AbstractString` : the relative or absolute path to the CSV file. - - - - # Examples: - - ```julia-repl - - julia> c = read_csv("./mydata/UBW_curric.csv") - - julia> dp = read_csv("./mydata/UBW_plan.csv") - - ``` - - """ - 6 function read_csv(file_path::AbstractString) - 6 file_path = remove_empty_lines(file_path) - 6 if typeof(file_path) == Bool && !file_path - 0 return false - - end - - # dict_curric_degree_type = Dict("AA"=>AA, "AS"=>AS, "AAS"=>AAS, "BA"=>BA, "BS"=>BS, ""=>BS) - 6 dict_curric_system = Dict("semester"=>semester, "quarter"=>quarter, ""=>semester) - 6 dp_name = "" - 6 dp_add_courses = Array{Course,1}() - 6 curric_name = "" - 6 curric_inst = "" - 6 curric_dtype = "BS" - 6 curric_stype = dict_curric_system["semester"] - 6 curric_CIP = "" - 6 courses_header = 1 - 6 course_count = 0 - 6 additional_course_start=0 - 6 additional_course_count=0 - 6 learning_outcomes_start=0 - 6 learning_outcomes_count=0 - 6 curric_learning_outcomes_start=0 - 6 curric_learning_outcomes_count=0 - 6 part_missing_term=false - 6 output = "" - - # Open the CSV file and read in the basic information such as the type (curric or degreeplan), institution, degree type, etc - 6 open(file_path) do csv_file - 6 read_line = csv_line_reader(readline(csv_file), ',') - 6 courses_header += 1 - 6 if strip(read_line[1]) == "Curriculum" - 6 curric_name = read_line[2] - 6 read_line = csv_line_reader(readline(csv_file), ',') - 6 is_dp = read_line[1] == "Degree Plan" - 6 if is_dp - 2 dp_name = read_line[2] - 2 read_line = csv_line_reader(readline(csv_file), ',') - 2 courses_header += 1 - - end - 6 if read_line[1] == "Institution" - 6 curric_inst = read_line[2] - 6 read_line = csv_line_reader(readline(csv_file), ',') - 6 courses_header += 1 - - end - 6 if read_line[1] == "Degree Type" - 6 curric_dtype = read_line[2] - 6 read_line = csv_line_reader(readline(csv_file), ',') - 6 courses_header += 1 - - end - 6 if read_line[1] == "System Type" - 6 curric_stype = dict_curric_system[lowercase(read_line[2])] - 6 read_line = csv_line_reader(readline(csv_file), ',') - 6 courses_header += 1 - - end - 6 if read_line[1] == "CIP" - 6 curric_CIP = read_line[2] - 6 read_line = csv_line_reader(readline(csv_file), ',') - 6 courses_header += 1 - - end - 6 if read_line[1] == "Courses" - 6 courses_header += 1 - - else - 0 println("Could not find Courses") - 0 return false - - end - - - - # File isn't formatted correctly, couldn't find the curriculum field in Col A Row 1 - - else - 0 println("Could not find a Curriculum") - 0 return false - - end - - - - # This is the row containing Course ID, Course Name, Prefix, etc - 6 read_line = csv_line_reader(readline(csv_file), ',') - - - - # Checks that all courses have an ID, and counts the total number of courses - 216 while length(read_line) > 0 && read_line[1] != "Additional Courses" && read_line[1] != "Course Learning Outcomes" && - - read_line[1] != "Curriculum Learning Outcomes" && !startswith(read_line[1], "#") - - - - # Enforce that each course has an ID - 105 if length(read_line[1]) == 0 - - # skip blank lines - 0 if !any(x -> (x != "" && x != " " && x != ' '), read_line) - 0 read_line = csv_line_reader(readline(csv_file), ',') - 0 continue - - end - 0 println("All courses must have a Course ID (1)") - 0 return false - - end - - - - # Enforce that each course has an associated term if the file is a degree plan - 105 if (is_dp) - 20 if (length(read_line) == 10) - 0 error("Each Course in a Degree Plan must have an associated term." * - - "\nCourse with ID \'$(read_line[1])\' ($(read_line[2])) has no term.") - 0 return false - 20 elseif (read_line[11] == 0) - 0 error("Each Course in a Degree Plan must have an associated term." * - - "\nCourse with ID \'$(read_line[1])\' ($(read_line[2])) has no term.") - 0 return false - - end - - end - - - 105 course_count += 1 - 210 read_line = csv_line_reader(readline(csv_file), ',') - - end - - - 6 df_courses = CSV.File(file_path, header = courses_header, limit = course_count - 1, delim = ',', silencewarnings = true) |> DataFrame - 6 if nrow(df_courses) != nrow(unique(df_courses, Symbol("Course ID"))) - 0 println("All courses must have a unique Course ID (2)") - 0 return false - - end - 10 if !is_dp && Symbol("Term") in names(df_courses) - 0 println("Curriculum cannot have term information.") - 0 return false - - end - 6 df_all_courses = DataFrame() - 6 df_additional_courses = DataFrame() - 6 if length(read_line) > 0 && read_line[1] == "Additional Courses" - 1 if !is_dp - 0 println("Only Degree Plan can have additional courses") - 0 return false - - end - 1 additional_course_start = courses_header+course_count+1 - 1 read_line = csv_line_reader(readline(csv_file), ',') - 11 while length(read_line) > 0 && read_line[1] != "Course Learning Outcomes" && - - read_line[1] != "Curriculum Learning Outcomes" && !startswith(read_line[1], "#") - 5 additional_course_count += 1 - 5 read_line = csv_line_reader(readline(csv_file), ',') - - end - - end - 6 if additional_course_count > 1 - 1 df_additional_courses = CSV.File(file_path, header = additional_course_start, limit = additional_course_count - 1, delim = ',', silencewarnings = true) |> DataFrame - 1 df_all_courses = vcat(df_courses,df_additional_courses) - - else - 5 df_all_courses = df_courses - - end - - - 6 df_course_learning_outcomes="" - 6 if length(read_line)>0 && read_line[1] == "Course Learning Outcomes" - 0 learning_outcomes_start = additional_course_start+additional_course_count+1 - 0 read_line = csv_line_reader(readline(csv_file), ',') - 0 while length(read_line)>0 && !startswith(read_line[1],"#") && read_line[1] != "Curriculum Learning Outcomes" - 0 learning_outcomes_count +=1 - 0 read_line = csv_line_reader(readline(csv_file), ',') - - end - 0 if learning_outcomes_count > 1 - 0 df_course_learning_outcomes = CSV.File(file_path, header = learning_outcomes_start, limit = learning_outcomes_count - 1, delim = ',', silencewarnings = true) |> DataFrame - - end - - end - 6 course_learning_outcomes = Dict{Int, Array{LearningOutcome}}() - 6 if df_course_learning_outcomes != "" - 0 course_learning_outcomes = generate_course_lo(df_course_learning_outcomes) - 0 if typeof(course_learning_outcomes) == Bool && !course_learning_outcomes - 0 return false - - end - - end - - - 6 df_curric_learning_outcomes = "" - 6 if length(read_line)>0 && read_line[1] == "Curriculum Learning Outcomes" - 0 curric_learning_outcomes_start = learning_outcomes_start+learning_outcomes_count+1 - 0 read_line = csv_line_reader(readline(csv_file), ',') - 0 while length(read_line)>0 && !startswith(read_line[1],"#") - 0 curric_learning_outcomes_count +=1 - 0 read_line = csv_line_reader(readline(csv_file), ',') - - end - 0 if learning_outcomes_count > 1 - 0 df_curric_learning_outcomes = CSV.File(file_path, header = curric_learning_outcomes_start, limit = curric_learning_outcomes_count - 1, delim = ',', silencewarnings = true) |> DataFrame - - end - - end - - - 18 curric_learning_outcomes = if df_curric_learning_outcomes != "" generate_curric_lo(df_curric_learning_outcomes) else LearningOutcome[] end - - - 6 if is_dp - 2 all_courses = read_all_courses(df_all_courses, course_learning_outcomes) - 2 if typeof(all_courses) == Bool && !all_courses - 0 return false - - end - 4 all_courses_arr = [course[2] for course in all_courses] - 2 additional_courses = read_courses(df_additional_courses, all_courses) - 2 ac_arr = Course[] - 3 for course in additional_courses - 7 push!(ac_arr, course[2]) - - end - 2 curric = Curriculum(curric_name, all_courses_arr, learning_outcomes = curric_learning_outcomes, degree_type = curric_dtype, - - system_type=curric_stype, institution=curric_inst, CIP=curric_CIP) - 2 terms = read_terms(df_all_courses, all_courses, all_courses_arr) - - #If some courses has term informations but some does not - 2 if isa(terms, Tuple) - - #Add curriculum to the output tuple - 0 output = (terms..., curric, dp_name, ac_arr) # ... operator enumrates the terms - - else - 2 degree_plan = DegreePlan(dp_name, curric, terms, ac_arr) - 2 output = degree_plan - - end - - else - 4 curric_courses = read_all_courses(df_courses, course_learning_outcomes) - 4 if typeof(curric_courses) == Bool && !curric_courses - 0 return false - - end - 8 curric_courses_arr = [course[2] for course in curric_courses] - 4 curric = Curriculum(curric_name, curric_courses_arr, learning_outcomes = curric_learning_outcomes, degree_type = curric_dtype, - - system_type=curric_stype, institution=curric_inst, CIP=curric_CIP) - 4 output = curric - - end - - end - - # Current file is the temp file created by remove_empty_lines(), remove the file. - 12 if file_path[end-8:end] == "_temp.csv" - 6 GC.gc() - 6 rm(file_path) - - end - 6 return output - - - - end - - - - """ - - write_csv(c::Curriculum, file_path::AbstractString) - - - - Write (i.e., serialize) a `Curriculum` data object to disk as a CSV file. To read - - (i.e., deserialize) a curriculum CSV file, use the corresponding `read_csv` function. - - The file format used to store curricula is described in [File Format](@ref). - - - - # Arguments - - - `c::Curriculum` : the `Curriculum` data object to be serialized. - - - `file_path::AbstractString` : the absolute or relative path where the CSV file will be stored. - - - - # Examples: - - ```julia-repl - - julia> write_csv(c, "./mydata/UBW_curric.csv") - - ``` - - """ - 2 function write_csv(curric::Curriculum, file_path::AbstractString; iostream=false, metrics=false) - 2 if iostream == true - 0 csv_file = IOBuffer() - 0 write_csv_content(csv_file, curric, false, metrics=metrics) - 0 return csv_file - - else - 1 open(file_path, "w") do csv_file - 1 write_csv_content(csv_file, curric, false, metrics=metrics) - - end - 1 return true - - end - - end - - - - # TODO - Reduce duplicated code between this and the curriculum version of the function - - """ - - write_csv(dp::DegreePlan, file_path::AbstractString) - - - - Write (i.e., serialize) a `DegreePlan` data object to disk as a CSV file. To read - - (i.e., deserialize) a degree plan CSV file, use the corresponding `read_csv` function. - - The file format used to store degree plans is described in [File Format](@ref). - - - - # Arguments - - - `dp::DegreePlan` : the `DegreePlan` data object to be serialized. - - - `file_path::AbstractString` : the absolute or relative path where the CSV file will be stored. - - - - # Examples: - - ```julia-repl - - julia> write_csv(dp, "./mydata/UBW_plan.csv") - - ``` - - """ - 2 function write_csv(original_plan::DegreePlan, file_path::AbstractString; iostream=false, metrics=false) - 2 if iostream - 0 csv_file = IOBuffer() - 0 write_csv_content(csv_file, original_plan, true, metrics=metrics) - - else - 1 open(file_path, "w") do csv_file - 1 write_csv_content(csv_file, original_plan, true, metrics=metrics) - - end - 1 return true - - end - - end - - - - - 4 function write_csv_content(csv_file, program, is_degree_plan; metrics=false) - - # dict_curric_degree_type = Dict(AA=>"AA", AS=>"AS", AAS=>"AAS", BA=>"BA", BS=>"BS") - 4 dict_curric_system = Dict(semester=>"semester", quarter=>"quarter") - - # Write Curriculum Name - 2 if is_degree_plan - - # Grab a copy of the curriculum - 1 curric = program.curriculum - 1 curric_name = "Curriculum," * "\""*string(curric.name) *"\""* ",,,,,,,,," - - else - 1 curric = program - 1 curric_name = "Curriculum," * string(curric.name) * ",,,,,,,,," - - end - 2 write(csv_file, curric_name) - - - - # Write Degree Plan Name - 2 if is_degree_plan - 1 dp_name = "\nDegree Plan," * "\""*string(program.name) *"\""* ",,,,,,,,," - 1 write(csv_file, dp_name) - - end - - - - # Write Institution Name - 2 curric_ins = "\nInstitution," * "\""*string(curric.institution) *"\""* ",,,,,,,,," - 2 write(csv_file, curric_ins) - - - - # Write Degree Type - 2 curric_dtype = "\nDegree Type," *"\""* string(curric.degree_type) * "\""*",,,,,,,,," - 2 write(csv_file,curric_dtype) - - - - # Write System Type (Semester or Quarter) - 2 curric_stype = "\nSystem Type," * "\""*string(dict_curric_system[curric.system_type]) * "\""*",,,,,,,,," - 2 write(csv_file, curric_stype) - - - - # Write CIP Code - 2 curric_CIP = "\nCIP," * "\""*string(curric.CIP) * "\""*",,,,,,,,," - 2 write(csv_file, curric_CIP) - - - - # Define course header - 2 if is_degree_plan - 1 if metrics - 0 course_header="\nCourse ID,Course Name,Prefix,Number,Prerequisites,Corequisites,Strict-Corequisites,Credit Hours,Institution,Canonical Name,Term,Complexity,Blocking,Delay,Centrality" - - else - - # 11 cols for degree plans (including term) - 2 course_header = "\nCourse ID,Course Name,Prefix,Number,Prerequisites,Corequisites,Strict-Corequisites,Credit Hours,Institution,Canonical Name,Term" - - end - - else - 1 if metrics - 0 course_header="\nCourse ID,Course Name,Prefix,Number,Prerequisites,Corequisites,Strict-Corequisites,Credit Hours,Institution,Canonical Name,Complexity,Blocking,Delay,Centrality" - - else - - # 10 cols for curricula (no term) - 1 course_header="\nCourse ID,Course Name,Prefix,Number,Prerequisites,Corequisites,Strict-Corequisites,Credit Hours,Institution,Canonical Name" - - end - - end - 2 write(csv_file, "\nCourses,,,,,,,,,,") - 2 write(csv_file, course_header) - - - - # Define dict to store all course learning outcomes - 2 all_course_lo = Dict{Int, Array{LearningOutcome, 1}}() - - - - # if metrics is true ensure that all values are present before writing courses - 2 if metrics - 0 complexity(curric) - 0 blocking_factor(curric) - 0 delay_factor(curric) - 0 centrality(curric) - - end - - - - # write courses (and additional courses for degree plan) - 2 if is_degree_plan - - # Iterate through each term and each course in the term and write them to the degree plan - 1 for (term_id, term) in enumerate(program.terms) - 3 for course in term.courses - 6 if !isdefined(program, :additional_courses) || !find_courses(program.additional_courses, course.id) - 6 write(csv_file, course_line(course, term_id, metrics = metrics)) - - end - - end - - end - - # Check if the original plan has additional courses defined - 1 if isdefined(program, :additional_courses) - - # Write the additional courses section of the CSV - 0 write(csv_file, "\nAdditional Courses,,,,,,,,,,") - 0 write(csv_file, course_header) - - # Iterate through each term - 0 for (term_id, term) in enumerate(program.terms) - - # Iterate through each course in the current term - 0 for course in term.courses - - # Check if the current course is an additional course, if so, write it here - 0 if find_courses(program.additional_courses, course.id) - 0 write(csv_file, course_line(course, term_id, metrics = metrics)) - - end - - # Check if the current course has learning outcomes, if so store them - 0 if length(course.learning_outcomes) > 0 - 0 all_course_lo[course.id] = course.learning_outcomes - - end - - end - - end - - end - - else - - # Iterate through each course in the curriculum - 1 for course in curric.courses - - # Write the current course to the CSV - 6 write(csv_file, course_line(course, "", metrics = metrics)) - - # Check if the course has learning outcomes, if it does store them - 6 if length(course.learning_outcomes) > 0 - 1 all_course_lo[course.id] = course.learning_outcomes - - end - - end - - end - - - - # Write course and curriculum learning outcomes, if any - 2 write_learning_outcomes(curric, csv_file, all_course_lo) - - end diff --git a/src/DataTypes/Course.jl.33665.cov b/src/DataTypes/Course.jl.33665.cov deleted file mode 100644 index af8721b..0000000 --- a/src/DataTypes/Course.jl.33665.cov +++ /dev/null @@ -1,204 +0,0 @@ - - # Course-related data types: - - # - - # AbstractCourse - - # / \ - - # Course CourseCollection - - # - - # A requirement may involve a set of courses (CourseSet), or a set of requirements (RequirementSet), but not both. - - """ - - The `AbstractCourse` data type is used to represent the notion of an abstract course that may appear in a curriculum - - or degree plan. That is, this abstract type serves as a placeholder for a course in a curriculum or degree plan, - - where the abstract course may correspond to a single course, or a set of courses, where only one of the courses in the - - set should be taken at that particular point in the curriculum or degree plan. This allows a user to specify a course - - or a collection of courses as a part part of a curriculum or degree plans. The two possible concrete subtypes of - - an `AbstractCourse` are: - - - `Course` : a specific course. - - - `CourseCollection` : a set of courses, any of which can serve as the required course in a curriculum or degree plan. - - """ - - - - abstract type AbstractCourse end - - - - ############################################################## - - # Course data type - - """ - - The `Course` data type is used to represent a single course consisting of a given number - - of credit hours. To instantiate a `Course` use: - - - - Course(name, credit_hours; ) - - - - # Arguments - - Required: - - - `name::AbstractString` : the name of the course. - - - `credit_hours::int` : the number of credit hours associated with the course. - - Keyword: - - - `prefix::AbstractString` : the prefix associated with the course. - - - `num::AbstractString` : the number associated with the course. - - - `institution:AbstractString` : the name of the institution offering the course. - - - `canonical_name::AbstractString` : the common name used for the course. - - - - # Examples: - - ```julia-repl - - julia> Course("Calculus with Applications", 4, prefix="MA", num="112", canonical_name="Calculus I") - - ``` - - """ - - mutable struct Course <: AbstractCourse - - id::Int # Unique course id - - vertex_id::Dict{Int, Int} # The vertex id of the course w/in a curriculum graph, stored as - - # (curriculum_id, vertex_id) - - name::AbstractString # Name of the course, e.g., Introduction to Psychology - - credit_hours::Real # Number of credit hours associated with course. For the - - # purpose of analytics, variable credits are not supported - - prefix::AbstractString # Typcially a department prefix, e.g., PSY - - num::AbstractString # Course number, e.g., 101, or 302L - - institution::AbstractString # Institution offering the course - - college::AbstractString # College or school (within the institution) offering the course - - department::AbstractString # Department (within the school or college) offering the course - - cross_listed::Array{Course} # courses that are cross-listed with the course (same as "also offered as") - - canonical_name::AbstractString # Standard name used to denote the course in the - - # discipline, e.g., Psychology I - - requisites::Dict{Int, Requisite} # List of requisites, in (requisite_course id, requisite_type) format - - learning_outcomes::Array{LearningOutcome} # A list of learning outcomes associated with the course - - metrics::Dict{String, Any} # Course-related metrics - - metadata::Dict{String, Any} # Course-related metadata - - - - passrate::Float64 # Percentage of students that pass the course - - - - # Constructor - 384 function Course(name::AbstractString, credit_hours::Real; prefix::AbstractString="", learning_outcomes::Array{LearningOutcome}=Array{LearningOutcome,1}(), - - num::AbstractString="", institution::AbstractString="", college::AbstractString="", department::AbstractString="", - - cross_listed::Array{Course}=Array{Course,1}(), canonical_name::AbstractString="", id::Int=0, passrate::Float64=0.5) - 384 this = new() - 192 this.name = name - 192 this.credit_hours = credit_hours - 192 this.prefix = prefix - 192 this.num = num - 192 this.institution = institution - 192 if id == 0 - 89 this.id = mod(hash(this.name * this.prefix * this.num * this.institution), UInt32) - - else - 103 this.id = id - - end - 192 this.college = college - 192 this.department = department - 192 this.cross_listed = cross_listed - 192 this.canonical_name = canonical_name - 192 this.requisites = Dict{Int, Requisite}() - - #this.requisite_formula - 192 this.metrics = Dict{String, Any}() - 192 this.metadata = Dict{String, Any}() - 192 this.learning_outcomes = learning_outcomes - 192 this.vertex_id = Dict{Int, Int}() # curriculum id -> vertex id, note: course may be in multiple curricula - - - - - 192 this.passrate = passrate - 192 return this - - end - - end - - - - mutable struct CourseCollection <: AbstractCourse - - id::Int # Unique course id - - vertex_id::Dict{Int, Int} # The vertex id of the course w/in a curriculum graph, stored as - - # (curriculum_id, vertex_id) - - courses::Array{Course} # Courses associated with the collection - - name::AbstractString # Name of the course, e.g., Introduction to Psychology - - credit_hours::Real # Number of credit hours associated with a "typcial" course in the collection - - institution::AbstractString # Institution offering the course - - college::AbstractString # College or school (within the institution) offering the course - - department::AbstractString # Department (within the school or college) offering the course - - canonical_name::AbstractString # Standard name used to denote the course collection, e.g., math genearl education - - requisites::Dict{Int, Requisite} # List of requisites, in (requisite_course id, requisite_type) format - - metrics::Dict{String, Any} # Course-related metrics - - metadata::Dict{String, Any} # Course-related metadata - - - - # Constructor - 2 function CourseCollection(name::AbstractString, credit_hours::Real, courses::Array{Course,1}; institution::AbstractString="", - - college::AbstractString="", department::AbstractString="", canonical_name::AbstractString="", id::Int=0) - 2 this = new() - 1 this.name = name - 1 this.credit_hours = credit_hours - 1 this.courses = courses - 1 this.institution = institution - 1 if id == 0 - 1 this.id = mod(hash(this.name * this.institution * string(length(courses))), UInt32) - - else - 0 this.id = id - - end - 1 this.college = college - 1 this.department = department - 1 this.canonical_name = canonical_name - 1 this.requisites = Dict{Int, Requisite}() - - #this.requisite_formula - 1 this.metrics = Dict{String, Any}() - 1 this.metadata = Dict{String, Any}() - 1 this.vertex_id = Dict{Int, Int}() # curriculum id -> vertex id - 1 return this - - end - - end - - - 1 function course_id(prefix::AbstractString, num::AbstractString, name::AbstractString, institution::AbstractString) - 1 convert(Int, mod(hash(name * prefix * num * institution), UInt32)) - - end - - - - """ - - add_requisite!(rc, tc, requisite_type) - - - - Add course rc as a requisite, of type requisite_type, for target course tc. - - - - # Arguments - - Required: - - - `rc::AbstractCourse` : requisite course. - - - `tc::AbstractCourse` : target course, i.e., course for which `rc` is a requisite. - - - `requisite_type::Requisite` : requisite type. - - - - # Requisite types - - One of the following requisite types must be specified for the `requisite_type`: - - - `pre` : a prerequisite course that must be passed before `tc` can be attempted. - - - `co` : a co-requisite course that may be taken before or at the same time as `tc`. - - - `strict_co` : a strict co-requisite course that must be taken at the same time as `tc`. - - """ - 159 function add_requisite!(requisite_course::AbstractCourse, course::AbstractCourse, requisite_type::Requisite) - 159 course.requisites[requisite_course.id] = requisite_type - - end - - - - """ - - add_requisite!([rc1, rc2, ...], tc, [requisite_type1, requisite_type2, ...]) - - - - Add a collection of requisites to target course tc. - - - - # Arguments - - Required: - - - `rc::Array{AbstractCourse}` : and array of requisite courses. - - - `tc::AbstractCourse` : target course, i.e., course for which `rc` is a requisite. - - - `requisite_type::Array{Requisite}` : an array of requisite types. - - - - # Requisite types - - The following requisite types may be specified for the `requisite_type`: - - - `pre` : a prerequisite course that must be passed before `tc` can be attempted. - - - `co` : a co-requisite course that may be taken before or at the same time as `tc`. - - - `strict_co` : a strict co-requisite course that must be taken at the same time as `tc`. - - """ - 2 function add_requisite!(requisite_courses::Array{AbstractCourse}, course::AbstractCourse, requisite_types::Array{Requisite}) - 2 @assert length(requisite_courses) == length(requisite_types) - 4 for i = 1:length(requisite_courses) - 6 course.requisites[requisite_courses[i].id] = requisite_types[i] - - end - - end - - - - """ - - delete_requisite!(rc, tc) - - - - Remove course rc as a requisite for target course tc. If rc is not an existing requisite for tc, an - - error is thrown. - - - - # Arguments - - Required: - - - `rc::AbstractCourse` : requisite course. - - - `tc::AbstractCourse` : target course, i.e., course for which `rc` is a requisite. - - - - """ - 1 function delete_requisite!(requisite_course::Course, course::Course) - - #if !haskey(course.requisites, requisite_course.id) - - # error("The requisite you are trying to delete does not exist") - - #end - 1 delete!(course.requisites, requisite_course.id) - - end diff --git a/src/DataTypes/CourseCatalog.jl.33665.cov b/src/DataTypes/CourseCatalog.jl.33665.cov deleted file mode 100644 index 0d057dd..0000000 --- a/src/DataTypes/CourseCatalog.jl.33665.cov +++ /dev/null @@ -1,56 +0,0 @@ - - ############################################################## - - # Course Catalog data type - - # Stores the collection of courses available at an institution - - mutable struct CourseCatalog - - id::Int # Unique course catalog ID - - name::AbstractString # Name of the course catalog - - institution::AbstractString # Institution offering the courses in the catalog - - date_range::Tuple # range of dates the catalog is applicable over - - catalog::Dict{Int, Course} # dictionary of courses in (course_id, course) format - - - - # Constructor - 28 function CourseCatalog(name::AbstractString, institution::AbstractString; courses::Array{Course}=Array{Course,1}(), - - catalog::Dict{Int,Course}=Dict{Int,Course}(), date_range::Tuple=(), id::Int=0) - 28 this = new() - 14 this.name = name - 14 this.institution = institution - 14 this.catalog = catalog - 14 this.date_range = date_range - 14 this.id = mod(hash(this.name * this.institution), UInt32) - 14 length(courses) > 0 ? add_course!(this, courses) : nothing - 14 return this - - end - - end - - - - # add a course to a course catalog, if the course is already in the catalog, it is not added again - 7 function add_course!(cc::CourseCatalog, course::Course) - 7 !is_duplicate(cc, course) ? cc.catalog[course.id] = course : nothing - - end - - - 5 function add_course!(cc::CourseCatalog, courses::Array{Course,1}) - 5 for course in courses - 7 add_course!(cc, course) - - end - - end - - - 9 function is_duplicate(cc::CourseCatalog, course::Course) - 16 course.id in keys(cc.catalog) ? true : false - - end - - - - function course(catalog::CourseCatalog, prefix::AbstractString, num::AbstractString) - - index = findfirst(c -> c.prefix == prefix && c.num == num, catalog.catalog) - - if index === nothing - - return false - - end - - return catalog.catalog[index] - - end - - - - # Return a course in a course catalog - 1 function course(cc::CourseCatalog, prefix::AbstractString, num::AbstractString, name::AbstractString) - 1 hash_val = mod(hash(name * prefix * num * cc.institution), UInt32) - 1 if hash_val in keys(cc.catalog) - 1 return cc.catalog[hash_val] - - else - 0 error("Course: $prefix $num: $name at $(cc.institution) does not exist in catalog: $(cc.name)") - - end - - end diff --git a/src/DataTypes/Curriculum.jl.33665.cov b/src/DataTypes/Curriculum.jl.33665.cov deleted file mode 100644 index 26d7dbf..0000000 --- a/src/DataTypes/Curriculum.jl.33665.cov +++ /dev/null @@ -1,396 +0,0 @@ - - ############################################################## - - # Curriculum data type - - # The required curriculum associated with a degree program - - """ - - The `Curriculum` data type is used to represent the collection of courses that must be - - be completed in order to earn a particualr degree. Thus, we use the terms *curriculum* and - - *degree program* synonymously. To instantiate a `Curriculum` use: - - - - Curriculum(name, courses; ) - - - - # Arguments - - Required: - - - `name::AbstractString` : the name of the curriculum. - - - `courses::Array{Course}` : the collection of required courses that comprise the curriculum. - - Keyword: - - - `degree_type::AbstractString` : the type of degree, e.g. BA, BBA, BSc, BEng, etc. - - - `institution:AbstractString` : the name of the institution offering the curriculum. - - - `system_type::System` : the type of system the institution uses, allowable - - types: `semester` (default), `quarter`. - - - `CIP::AbstractString` : the Classification of Instructional Programs (CIP) code for the - - curriculum. See: `https://nces.ed.gov/ipeds/cipcode` - - - - # Examples: - - ```julia-repl - - julia> Curriculum("Biology", courses, institution="South Harmon Tech", degree_type=AS, CIP="26.0101") - - ``` - - """ - - mutable struct Curriculum - - id::Int # Unique curriculum ID - - name::AbstractString # Name of the curriculum (can be used as an identifier) - - institution::AbstractString # Institution offering the curriculum - - degree_type::AbstractString # Type of degree_type - - system_type::System # Semester or quarter system - - CIP::AbstractString # CIP code associated with the curriculum - - courses::Array{AbstractCourse} # Array of required courses in curriculum - - num_courses::Int # Number of required courses in curriculum - - credit_hours::Real # Total number of credit hours in required curriculum - - graph::SimpleDiGraph{Int} # Directed graph representation of pre-/co-requisite structure - - # of the curriculum, note: this is a course graph - - learning_outcomes::Array{LearningOutcome} # A list of learning outcomes associated with the curriculum - - learning_outcome_graph::SimpleDiGraph{Int} # Directed graph representatin of pre-/co-requisite structure of learning - - # outcomes in the curriculum - - course_learning_outcome_graph::MetaDiGraph{Int} # Directed Int64 metagraph with Float64 weights defined by :weight (default weight 1.0) - - # This is a course and learning outcome graph - - metrics::Dict{String, Any} # Curriculum-related metrics - - metadata::Dict{String, Any} # Curriculum-related metadata - - - - # Constructor - 42 function Curriculum(name::AbstractString, courses::Array{AbstractCourse}; learning_outcomes::Array{LearningOutcome}=Array{LearningOutcome,1}(), - - degree_type::AbstractString="BS", system_type::System=semester, institution::AbstractString="", CIP::AbstractString="", - - id::Int=0, sortby_ID::Bool=true) - 42 this = new() - 21 this.name = name - 21 this.degree_type = degree_type - 21 this.system_type = system_type - 21 this.institution = institution - 21 if id == 0 - 21 this.id = mod(hash(this.name * this.institution * string(this.degree_type)), UInt32) - - else - 0 this.id = id - - end - 21 this.CIP = CIP - 21 if sortby_ID - 1081 this.courses = sort(collect(courses), by = c -> c.id) - - else - 8 this.courses = courses - - end - 21 this.num_courses = length(this.courses) - 21 this.credit_hours = total_credits(this) - 21 this.graph = SimpleDiGraph{Int}() - 21 create_graph!(this) - 21 this.metrics = Dict{String, Any}() - 21 this.metadata = Dict{String, Any}() - 21 this.learning_outcomes = learning_outcomes - 21 this.learning_outcome_graph = SimpleDiGraph{Int}() - 21 create_learning_outcome_graph!(this) - 21 this.course_learning_outcome_graph = MetaDiGraph() - 21 create_course_learning_outcome_graph!(this) - 21 errors = IOBuffer() - 21 if !(is_valid(this, errors)) - 3 printstyled("WARNING: Curriculum was created, but is invalid due to requisite cycle(s):", color = :yellow) - 3 println(String(take!(errors))) - - end - 21 return this - - end - - - 42 function Curriculum(name::AbstractString, courses::Array{Course}; learning_outcomes::Array{LearningOutcome}=Array{LearningOutcome,1}(), - - degree_type::AbstractString="BS", system_type::System=semester, institution::AbstractString="", CIP::AbstractString="", - - id::Int=0, sortby_ID::Bool=true) - 42 Curriculum(name, convert(Array{AbstractCourse},courses), learning_outcomes=learning_outcomes, degree_type=degree_type, - - system_type=system_type, institution=institution, CIP=CIP, id=id, sortby_ID=sortby_ID) - - end - - end - - - - # Check if a curriculum graph has requisite cycles. - - """ - - is_valid(c::Curriculum, errors::IOBuffer) - - - - Tests whether or not the curriculum graph ``G_c`` associated with curriculum `c` is valid, i.e., - - whether or not it contains a requisite cycle, or requisites that cannot be satisfied. Returns - - a boolean value, with `true` indicating the curriculum is valid, and `false` indicating it is not. - - - - If ``G_c`` is not valid, the `errors` buffer. To view these errors, use: - - - - ```julia-repl - - julia> errors = IOBuffer() - - julia> is_valid(c, errors) - - julia> println(String(take!(errors))) - - ``` - - - - A curriculum graph is not valid if it contains a directed cycle or unsatisfiable requisites; in this - - case it is not possible to complete the curriculum. For the case of unsatisfiable requistes, consider - - two courses ``c_1`` and ``c_2``, with ``c_1`` a prerequisite for ``c_2``. If a third course ``c_3`` - - is a strict corequisite for ``c_2``, as well as a requisite for ``c_1`` (or a requisite for any course - - on a path leading to ``c_2``), then the set of requisites cannot be satisfied. - - """ - 27 function is_valid(c::Curriculum, error_msg::IOBuffer=IOBuffer()) - 27 g = deepcopy(c.graph) - 27 validity = true - - # First check for simple cycles - 27 cycles = simplecycles(g) - - # Next check for cycles that could be created by strict co-requisites. - - # For every strict-corequisite in the curriculum, add another strict-corequisite between the same two vertices, but in - - # the opposite direction. If this creates any cycles of length greater than 2 in the modified graph (i.e., involving - - # more than the two courses in the strict-corequisite relationship), then the curriculum is unsatisfiable. - 27 for course in c.courses - 269 for (k,r) in course.requisites - 250 if r == strict_co - 27 v_d = course_from_id(c,course.id).vertex_id[c.id] # destination vertex - 27 v_s = course_from_id(c,k).vertex_id[c.id] # source vertex - 54 add_edge!(g, v_d, v_s) - - end - - end - - end - 27 new_cycles = simplecycles(g) - 27 idx = [] - 38 for (i,cyc) in enumerate(new_cycles) # remove length-2 cycles - 35 if length(cyc) == 2 - 51 push!(idx, i) - - end - - end - 27 deleteat!(new_cycles, idx) - 27 cycles = union(new_cycles, cycles) # remove redundant cycles - 27 if length(cycles) != 0 - 6 validity = false - 6 c.institution != "" ? write(error_msg, "\n$(c.institution): ") : "\n" - 6 write(error_msg, " curriculum \'$(c.name)\' has requisite cycles:\n") - 6 for cyc in cycles - 8 write(error_msg, "(") - 16 for (i,v) in enumerate(cyc) - 20 if i != length(cyc) - 12 write(error_msg, "$(c.courses[v].name), ") - - else - 26 write(error_msg, "$(c.courses[v].name))\n") - - end - - end - - end - - end - 27 return validity - - end - - - - # TODO: This function should be depracated on next major version release - - function isvalid_curriculum(c::Curriculum, error_msg::IOBuffer=IOBuffer()) - - println("isvalid_curriculum() will be depracated, use is_valid() instead.") - - return is_valid(c, error_msg) - - end - - - - # TODO: update a curriculum graph if requisites have been added/removed or courses have been added/removed - - #function update_curriculum(curriculum::Curriculum, courses::Array{Course}=()) - - # # if courses array is empty, no new courses were added - - #end - - - - # Converts course ids, from those used in CSV file format, to the standard hashed id used by the data structures in the toolbox - 2 function convert_ids(curriculum::Curriculum) - 2 for c1 in curriculum.courses - 20 old_id = c1.id - 20 c1.id = mod(hash(c1.name * c1.prefix * c1.num * c1.institution), UInt32) - 20 if old_id != c1.id - 12 for c2 in curriculum.courses - 144 if old_id in keys(c2.requisites) - 8 add_requisite!(c1, c2, c2.requisites[old_id]) - 10 delete!(c2.requisites, old_id) - - end - - end - - end - - end - 2 return curriculum - - end - - - - # Map course IDs to vertex IDs in an underlying curriculum graph. - 43 function map_vertex_ids(curriculum::Curriculum) - 43 mapped_ids = Dict{Int, Int}() - 43 for c in curriculum.courses - 433 mapped_ids[c.id] = c.vertex_id[curriculum.id] - - end - 43 return mapped_ids - - end - - - - # Map lo IDs to vertex IDs in an underlying curriculum graph. - 42 function map_lo_vertex_ids(curriculum::Curriculum) - 42 mapped_ids = Dict{Int, Int}() - 84 for lo in curriculum.learning_outcomes - 0 mapped_ids[lo.id] = lo.vertex_id[curriculum.id] - - end - 42 return mapped_ids - - end - - - - # Compute the hash value used to create the id for a course, and return the course if it exists in the curriculum supplied as input - 2 function course(curric::Curriculum, prefix::AbstractString, num::AbstractString, name::AbstractString, institution::AbstractString) - 2 hash_val = mod(hash(name * prefix * num * institution), UInt32) - 2 if hash_val in collect(c.id for c in curric.courses) - 8 return curric.courses[findfirst(x->x.id==hash_val, curric.courses)] - - else - 0 error("Course: $prefix $num: $name at $institution does not exist in curriculum: $(curric.name)") - - end - - end - - - - # Return the course associated with a course id in a curriculum - 69 function course_from_id(curriculum::Curriculum, id::Int) - 69 for c in curriculum.courses - 1009 if c.id == id - 69 return c - - end - - end - - end - - - - # Return the lo associated with a lo id in a curriculum - 0 function lo_from_id(curriculum::Curriculum, id::Int) - 0 for lo in curriculum.learning_outcomes - 0 if lo.id == id - 0 return lo - - end - - end - - end - - - - # Return the course associated with a vertex id in a curriculum graph - 17 function course_from_vertex(curriculum::Curriculum, vertex::Int) - 17 c = curriculum.courses[vertex] - - end - - - - # The total number of credit hours in a curriculum - 22 function total_credits(curriculum::Curriculum) - 22 total_credits = 0 - 22 for c in curriculum.courses - 221 total_credits += c.credit_hours - - end - 22 return total_credits - - end - - - - #""" - - # create_graph!(c::Curriculum) - - # - - #Create a curriculum directed graph from a curriculum specification. The graph is stored as a - - #LightGraph.jl implemenation within the Curriculum data object. - - #""" - 21 function create_graph!(curriculum::Curriculum) - 21 for (i, c) in enumerate(curriculum.courses) - 191 if add_vertex!(curriculum.graph) - 191 c.vertex_id[curriculum.id] = i # The vertex id of a course w/in the curriculum - - # Graphs.jl orders graph vertices sequentially - - # TODO: make sure course is not alerady in the curriculum - - else - 191 error("vertex could not be created") - - end - - end - 21 mapped_vertex_ids = map_vertex_ids(curriculum) - 21 for c in curriculum.courses - 191 for r in collect(keys(c.requisites)) - 166 if add_edge!(curriculum.graph, mapped_vertex_ids[r], c.vertex_id[curriculum.id]) - - else - 0 s = course_from_id(curriculum, r) - 166 error("edge could not be created: ($(s.name), $(c.name))") - - end - - end - - end - - end - - - - #""" - - # create_course_learning_outcome_graph!(c::Curriculum) - - # - - #Create a curriculum directed graph from a curriculum specification. This graph graph contains courses and learning outcomes - - # of the curriculum. The graph is stored as a LightGraph.jl implemenation within the Curriculum data object. - - - - - - #""" - 21 function create_course_learning_outcome_graph!(curriculum::Curriculum) - 21 len_courses = size(curriculum.courses)[1] - 21 len_learning_outcomes = size(curriculum.learning_outcomes)[1] - - - 21 for (i, c) in enumerate(curriculum.courses) - 191 if add_vertex!(curriculum.course_learning_outcome_graph) - 191 c.vertex_id[curriculum.id] = i # The vertex id of a course w/in the curriculum - - # Graphs.jl orders graph vertices sequentially - - # TODO: make sure course is not alerady in the curriculum - - else - 191 error("vertex could not be created") - - end - - - - end - - - 21 for (j, lo) in enumerate(curriculum.learning_outcomes) - 0 if add_vertex!(curriculum.course_learning_outcome_graph) - 0 lo.vertex_id[curriculum.id] = len_courses + j # The vertex id of a learning outcome w/in the curriculum - - # Graphs.jl orders graph vertices sequentially - - # TODO: make sure course is not alerady in the curriculum - - else - 0 error("vertex could not be created") - - end - - end - - - 21 mapped_vertex_ids = map_vertex_ids(curriculum) - 21 mapped_lo_vertex_ids = map_lo_vertex_ids(curriculum) - - - - - - # Add edges among courses - 21 for c in curriculum.courses - 191 for r in collect(keys(c.requisites)) - 166 if add_edge!(curriculum.course_learning_outcome_graph, mapped_vertex_ids[r], c.vertex_id[curriculum.id]) - 166 set_prop!(curriculum.course_learning_outcome_graph, Edge(mapped_vertex_ids[r], c.vertex_id[curriculum.id]), :c_to_c, c.requisites[r]) - - - - else - 0 s = course_from_id(curriculum, r) - 187 error("edge could not be created: ($(s.name), $(c.name))") - - end - - end - - end - - - - # Add edges among learning_outcomes - 42 for lo in curriculum.learning_outcomes - 0 for r in collect(keys(lo.requisites)) - 0 if add_edge!(curriculum.course_learning_outcome_graph, mapped_lo_vertex_ids[r], lo.vertex_id[curriculum.id]) - 0 set_prop!(curriculum.course_learning_outcome_graph, Edge(mapped_lo_vertex_ids[r], lo.vertex_id[curriculum.id]), :lo_to_lo, pre) - - else - 0 s = lo_from_id(curriculum, r) - 0 error("edge could not be created: ($(s.name), $(c.name))") - - end - - end - - end - - - - # Add edges between each pair of a course and a learning outcome - 21 for c in curriculum.courses - 191 for lo in c.learning_outcomes - 0 if add_edge!(curriculum.course_learning_outcome_graph, mapped_lo_vertex_ids[lo.id], c.vertex_id[curriculum.id]) - 0 set_prop!(curriculum.course_learning_outcome_graph, Edge(mapped_lo_vertex_ids[lo.id], c.vertex_id[curriculum.id]), :lo_to_c, belong_to) - - else - 0 s = lo_from_id(curriculum, lo.id) - 0 error("edge could not be created: ($(s.name), $(c.name))") - - end - - end - - end - - end - - - - #""" - - # create_learning_outcome_graph!(c::Curriculum) - - # - - #Create a curriculum directed graph from a curriculum specification. The graph is stored as a - - #LightGraph.jl implemenation within the Curriculum data object. - - #""" - 21 function create_learning_outcome_graph!(curriculum::Curriculum) - 21 for (i, lo) in enumerate(curriculum.learning_outcomes) - 0 if add_vertex!(curriculum.learning_outcome_graph) - 0 lo.vertex_id[curriculum.id] = i # The vertex id of a course w/in the curriculum - - # Graphs.jl orders graph vertices sequentially - - # TODO: make sure course is not alerady in the curriculum - - else - 0 error("vertex could not be created") - - end - - end - 21 mapped_vertex_ids = map_lo_vertex_ids(curriculum) - 42 for lo in curriculum.learning_outcomes - 0 for r in collect(keys(lo.requisites)) - 0 if add_edge!(curriculum.learning_outcome_graph, mapped_vertex_ids[r], lo.vertex_id[curriculum.id]) - - else - 0 s = lo_from_id(curriculum, r) - 0 error("edge could not be created: ($(s.name), $(c.name))") - - end - - end - - end - - end - - - - # find requisite type from vertex ids in a curriculum graph - 2 function requisite_type(curriculum::Curriculum, src_course_id::Int, dst_course_id::Int) - 4 src = 0; dst = 0 - 2 for c in curriculum.courses - 16 if c.vertex_id[curriculum.id] == src_course_id - 2 src = c - 14 elseif c.vertex_id[curriculum.id] == dst_course_id - 20 dst = c - - end - - end - 4 if ((src == 0 || dst == 0) || !haskey(dst.requisites, src.id)) - 0 error("edge ($src_course_id, $dst_course_id) does not exist in curriculum graph") - - else - 2 return dst.requisites[src.id] - - end - - end diff --git a/src/DataTypes/DegreePlan.jl.33665.cov b/src/DataTypes/DegreePlan.jl.33665.cov deleted file mode 100644 index 8abfcf9..0000000 --- a/src/DataTypes/DegreePlan.jl.33665.cov +++ /dev/null @@ -1,226 +0,0 @@ - - ############################################################## - - # Term data type - - """ - - The `Term` data type is used to represent a single term within a `DegreePlan`. To - - instantiate a `Term` use: - - - - Term([c1, c2, ...]) - - - - where c1, c2, ... are `Course` data objects - - """ - - mutable struct Term - - courses::Array{AbstractCourse} # The courses associated with a term in a degree plan - - num_courses::Int # The number of courses in the Term - - credit_hours::Real # The number of credit hours associated with the term - - metrics::Dict{String, Any} # Term-related metrics - - metadata::Dict{String, Any} # Term-related metadata - - - - # Constructor - 27 function Term(courses::Array{AbstractCourse}) - 27 this = new() - 27 this.num_courses = length(courses) - 27 this.courses = Array{AbstractCourse}(undef, this.num_courses) - 27 this.credit_hours = 0 - 54 for i = 1:this.num_courses - 62 this.courses[i] = courses[i] - 97 this.credit_hours += courses[i].credit_hours - - end - 27 this.metrics = Dict{String, Any}() - 27 this.metadata = Dict{String, Any}() - 27 return this - - end - - - 21 function Term(courses::Array{Course}) - 21 Term(convert(Array{AbstractCourse}, courses)) - - end - - - - end - - - - ############################################################## - - # Degree Plan data type - - """ - - The `DegreePlan` data type is used to represent the collection of courses that must be - - be completed in order to earn a particualr degree. To instantiate a `Curriculum` use: - - - - DegreePlan(name, curriculum, terms, additional_courses) - - - - # Arguments - - - `name::AbstractString` : the name of the degree plan. - - - `curriculum::Curriculum` : the curriculum the degree plan must satisfy. - - - `terms::Array{Term}` : the arrangement of terms associated with the degree plan. - - - `additional_courses::Array{Course}` : additional courses in the degree plan that are not - - a part of the curriculum. E.g., a prerequisite math class to the first required math - - class in the curriculum. - - - - # Examples: - - ```julia-repl - - julia> DegreePlan("Biology 4-year Degree Plan", curriculum, terms) - - ``` - - """ - - mutable struct DegreePlan - - name::AbstractString # Name of the degree plan - - curriculum::Curriculum # Curriculum the degree plan satisfies - - additional_courses::Array{AbstractCourse} # Additional (non-required) courses added to the degree plan, - - # e.g., these may be preparatory courses - - graph::SimpleDiGraph{Int} # Directed graph representation of pre-/co-requisite structure - - # of the degre plan - - terms::Array{Term} # The terms associated with the degree plan - - num_terms::Int # Number of terms in the degree plan - - credit_hours::Real # Total number of credit hours in the degree plan - - metrics::Dict{String, Any} # Dergee Plan-related metrics - - metadata::Dict{String, Any} # Dergee Plan-related metadata - - - - # Constructor - 16 function DegreePlan(name::AbstractString, curriculum::Curriculum, terms::Array{Term,1}, - - additional_courses::Array{<:AbstractCourse,1}=Array{AbstractCourse,1}()) - 16 this = new() - 9 this.name = name - 9 this.curriculum = curriculum - 9 this.num_terms = length(terms) - 9 this.terms = Array{Term}(undef, this.num_terms) - 9 this.credit_hours = 0 - 18 for i = 1:this.num_terms - 28 this.terms[i] = terms[i] - 47 this.credit_hours += terms[i].credit_hours - - end - 17 if isassigned(additional_courses) - 1 this.additional_courses = Array{AbstractCourse}(undef, length(additional_courses)) - 2 for i = 1:length(additional_courses) - 7 this.additional_courses[i] = additional_courses[i] - - end - - end - 9 this.metrics = Dict{String, Any}() - 9 this.metadata = Dict{String, Any}() - 9 return this - - end - - end - - - - # Check if a degree plan is valid. - - # Print error_msg using println(String(take!(error_msg))), where error_msg is the buffer returned by this function - - """ - - is_valid(plan::DegreePlan, errors::IOBuffer) - - - - Tests whether or not the degree plan `plan` is valid. Returns a boolean value, with `true` indicating the - - degree plan is valid, and `false` indicating it is not. - - - - If `plan` is not valid, the reason(s) why are written to the `errors` buffer. To view these - - reasons, use: - - - - ```julia-repl - - julia> errors = IOBuffer() - - julia> is_valid(plan, errors) - - julia> println(String(take!(errors))) - - ``` - - - - There are three reasons why a degree plan might not be valid: - - - - - Requisites not satsified : A prerequisite for a course occurs in a later term than the course itself. - - - Incomplete plan : There are course in the curriculum not included in the degree plan. - - - Redundant plan : The same course appears in the degree plan multiple times. - - - - """ - 6 function is_valid(plan::DegreePlan, error_msg::IOBuffer=IOBuffer()) - 6 validity = true - - # All requisite relationships are satisfied? - - # -no backwards pointing requisites - 6 for i in 2:plan.num_terms - 5 for c in plan.terms[i].courses - 20 for j in i-1:-1:1 - 14 for k in plan.terms[j].courses - 28 for l in keys(k.requisites) - 6 if l == c.id - 1 validity = false - 3 write(error_msg, "\n-Invalid requisite: $(c.name) in term $i is a requisite for $(k.name) in term $j") - - end - - end - - end - - end - - end - - end - - # -requisites within the same term must be corequisites - 6 for i in 1:plan.num_terms - 8 for c in plan.terms[i].courses - 16 for r in plan.terms[i].courses - 32 if c == r - 16 continue - 16 elseif haskey(c.requisites, r.id) - 2 if c.requisites[r.id] == pre - 0 validity = false - 37 write(error_msg, "\n-Invalid prerequisite: $(r.name) in term $i is a prerequisite for $(c.name) in the same term") - - end - - end - - end - - end - - end - - # -TODO: strict co-requisites must be in the same term - - # All courses in the curriculum are in the degree plan? - 3 curric_classes = Set() - 3 dp_classes = Set() - 3 for i in plan.curriculum.courses - 21 push!(curric_classes, i.id) - - end - 6 for i = 1:plan.num_terms - 8 for j in plan.terms[i].courses - 21 push!(dp_classes, j.id) - - end - - end - 3 if length(setdiff(curric_classes, dp_classes)) > 0 - 1 validity = false - 2 for i in setdiff(curric_classes, dp_classes) - 2 c = course_from_id(plan.curriculum, i) - 4 write(error_msg, "\n-Degree plan is missing required course: $(c.name)") - - end - - end - - # Is a course in the degree plan multiple times? - 3 dp_classes = Set() - 6 for i = 1:plan.num_terms - 8 for j in plan.terms[i].courses - 16 if in(j.id, dp_classes) - 0 validity = false - 0 write(error_msg, "\n-Course $(j.name) is listed multiple times in degree plan") - - else - 21 push!(dp_classes, j.id) - - end - - end - - end - 3 return validity - - end - - - - # TODO: This function should be depracated on next major version release - - function isvalid_degree_plan(plan::DegreePlan, error_msg::IOBuffer=IOBuffer()) - - println("isvalid_degree_plan() will be depracated, use is_valid() instead.") - - return is_valid(plan, error_msg) - - end - - - - """ - - find_term(plan::DegreePlan, course::Course) - - - - In degree plan `plan`, find the term in which course `course` appears. If `course` in not in the degree plan an - - error message is provided. - - """ - 25 function find_term(plan::DegreePlan, course::Course) - 25 for (i, term) in enumerate(plan.terms) - 45 if course in term.courses - 25 return i - - end - - end - 0 write(error_msg, "Course $(course.name) is not in the degree plan") - - end - - - - # ugly print of degree plan - - """ - - print_plan(plan::DegreePlan) - - - - Ugly print out of a degree plan to the Julia console. - - """ - 1 function print_plan(plan::DegreePlan) - 1 println("\nDegree Plan: $(plan.name) for $(plan.curriculum.degree_type) in $(plan.curriculum.name)\n") - 1 println(" $(plan.credit_hours) credit hours") - 2 for i = 1:plan.num_terms - 3 println(" Term $i courses:") - 3 for j in plan.terms[i].courses - 9 println(" $(j.name) ") - - end - 3 println("\n") - - end - - end diff --git a/src/DataTypes/LearningOutcome.jl.33665.cov b/src/DataTypes/LearningOutcome.jl.33665.cov deleted file mode 100644 index 4ece750..0000000 --- a/src/DataTypes/LearningOutcome.jl.33665.cov +++ /dev/null @@ -1,64 +0,0 @@ - - ############################################################## - - # LearningOutcome data type - - """ - - The `LearningOutcome` data type is used to associate a set of learning outcomes with - - a course or a curriculum (i.e., degree program). To instantiate a `LearningOutcome` use: - - - - LearningOutcome(name, description, hours) - - - - # Arguments - - - `name::AbstractString` : the name of the learning outcome. - - - `description::AbstractString` : detailed description of the learning outcome. - - - `hours::int` : number of class (contact) hours needed to attain the learning outcome. - - - - # Examples: - - ```julia-repl - - julia> LearningOutcome("M1", "Learner will demonstrate the ability to ...", 12) - - ``` - - """ - - mutable struct LearningOutcome - - id::Int # Unique id for the learning outcome, - - # set when the cousrse is added to a graph - - vertex_id::Dict{Int, Int} # The vertex id of the learning outcome w/in a curriculum graph, stored as - - # (curriculum_id, vertex_id) - - name::AbstractString # Name of the learning outcome - - description::AbstractString # A description of the learning outcome - - hours::Int # number of class hours that should be devoted - - # to the learning outcome - - requisites::Dict{Int, Requisite} # List of requisites, in (requisite_learning_outcome, requisite_type) format - - affinity::Dict{Int, Real} # Affinity to other learning outcomes in (LearningOutcome ID, affinity value) format, - - # where affinity is in the interval [0,1]. - - metrics::Dict{String, Any} # Learning outcome-related metrics - - metadata::Dict{String, Any} # Learning outcome-related metadata - - - - # Constructor - 4 function LearningOutcome(name::AbstractString, description::AbstractString, hours::Int=0) - 4 this = new() - 4 this.name =name - 4 this.description = description - 4 this.hours = hours - 4 this.id = mod(hash(this.name * this.description), UInt32) - 4 this.requisites = Dict{Int, Requisite}() - 4 this.metrics = Dict{String, Any}() - 4 this.metadata = Dict{String, Any}() - 4 this.vertex_id = Dict{Int, Int}() # curriculum id -> vertex id - 4 return this - - end - - end - - - - #""" - - #add_lo_requisite!(rlo, tlo, requisite_type) - - #Add learning outcome rlo as a requisite, of type requisite_type, for target learning outcome tlo - - #outcome tlo. - - #""" - 1 function add_lo_requisite!(requisite_lo::LearningOutcome, lo::LearningOutcome, requisite_type::Requisite) - 1 lo.requisites[requisite_lo.id] = requisite_type - - end - - - 1 function add_lo_requisite!(requisite_lo::Array{LearningOutcome}, lo::LearningOutcome, - - requisite_type::Array{Requisite}) - 1 @assert length(requisite_lo) == length(requisite_type) - 2 for i = 1:length(requisite_lo) - 2 lo.requisites[requisite_lo[i].id] = requisite_type[i] - - end - - end diff --git a/src/DataTypes/Requirements.jl.33665.cov b/src/DataTypes/Requirements.jl.33665.cov deleted file mode 100644 index fa3833a..0000000 --- a/src/DataTypes/Requirements.jl.33665.cov +++ /dev/null @@ -1,320 +0,0 @@ - - ############################################################## - - # DegreeRequirement data types - - - - # Create an integer data type called Grade - - Grade = UInt64 - - - - # function for converting a letter grade into a integer, divide by 3 to convert to 4-point GPA scale - 64 function grade(letter_grade::AbstractString) - 64 if letter_grade == "A➕" - 3 return convert(Grade, 13) - 61 elseif letter_grade == "A" - 2 return convert(Grade, 12) - 59 elseif letter_grade == "A➖" - 2 return convert(Grade, 11) - 57 elseif letter_grade == "B➕" - 2 return convert(Grade, 10) - 55 elseif letter_grade == "B" - 2 return convert(Grade, 9) - 53 elseif letter_grade == "B➖" - 2 return convert(Grade, 8) - 51 elseif letter_grade == "C➕" - 2 return convert(Grade, 7) - 49 elseif letter_grade == "C" - 12 return convert(Grade, 6) - 37 elseif letter_grade == "C➖" - 2 return convert(Grade, 5) - 35 elseif letter_grade == "D➕" - 2 return convert(Grade, 4) - 33 elseif letter_grade == "D" - 25 return convert(Grade, 3) - 8 elseif letter_grade == "D➖" - 2 return convert(Grade, 2) - 6 elseif letter_grade == "P" - 1 return convert(Grade, 0) - 5 elseif letter_grade == "F" - 1 return convert(Grade, 0) - 4 elseif letter_grade == "I" - 1 return convert(Grade, 0) - 3 elseif letter_grade == "WP" - 1 return convert(Grade, 0) - 2 elseif letter_grade == "W" - 1 return convert(Grade, 0) - 1 elseif letter_grade == "WF" - 1 return convert(Grade, 0) - - else - 0 error("letter grade $letter_grade is not supported") - - end - - end - - - - # function for converting an integer letter grade, divide by 3 to convert to 4-point GPA scale - 12 function grade(int_grade::Grade) - 12 if int_grade == 13 - 1 return "A➕" - 11 elseif int_grade == 12 - 1 return "A" - 10 elseif int_grade == 11 - 1 return "A➖" - 9 elseif int_grade == 10 - 1 return "B➕" - 8 elseif int_grade == 9 - 1 return "B" - 7 elseif int_grade == 8 - 1 return "B➖" - 6 elseif int_grade == 7 - 1 return "C➕" - 5 elseif int_grade == 6 - 1 return "C" - 4 elseif int_grade == 5 - 1 return "C➖" - 3 elseif int_grade == 4 - 1 return "D➕" - 2 elseif int_grade == 3 - 1 return "D" - 1 elseif int_grade == 2 - 1 return "D➖" - 0 elseif int_grade == 0 - 0 return "F" - - else - 0 error("grade value $int_grade is not supported") - - end - - end - - - - # Data types for requirements: - - # - - # AbstractRequirement - - # / \ - - # CourseSet RequirementSet - - # - - # A requirement may involve a set of courses (CourseSet), or a set of requirements (RequirementSet), but not both. - - """ - - The `AbstractRequirement` data type is used to represent the requirements associated with an academic program. A - - reqiurement may consist of either the set of courses that can be used to satisfy the requirement, or a set of - - requirements that all must be satisfied. That is, the two possible concrete subtypes of an `AbstractRequirement` - - are: - - - `CourseSet` : a set of courses that may be used to satisfy a requirement. - - - `RequirementSet` : a set of requirements that may be used to satisfied a requirement. The requirement set may - - consist of other RequirementSets or CourseSets (these are the children of the parent `RequirementSet`). - - - - A valid set of requirements for a degree program is created as a tree of requirements, where all leaves in the - - tree must be `CourseSet` objects. - - """ - - abstract type AbstractRequirement end - - - - """ - - The `CourseSet` data type is used to represent the requirements associated with an academic program. A - - reqiurement may consist of either the set of courses that can be used to satisfy the requirement, or a set of - - requirements that all must be satisfied. That is, a `Requirement` must be one of two types: - - - `course_set` : the set of courses that are used to satisfy the requirement are specifed in the `courses` array. - - - `requirement_set` : that set of requirements that must be satisfied are specified in the `requirements` array. - - - - To instantiate a `CourseSet` use: - - - - CourseSet(name, credit_hours, courses, description) - - - - # Arguments - - Required: - - - `name::AbstractString` : the name of the requirement. - - - `credit_hours::Real` : the number of credit hours associated with the requirement. - - - `course_reqs::Array{Pair{Course,Grade}} ` : the collection of courses that can be used to satisfy the requirement, - - and the minimum grade required in each. - - - - Keyword: - - - `description::AbstractString` : A detailed description of the requirement. Default is the empty string. - - - `course_catalog::CourseCatalog` : Course catalog to draw courses from using the `prefix_regex` and `num_regex` regular - - expressions (positive matches are added to the course_reqs array). Note: both `prefix_regex` and `num_regex` must - - match in order for a course to be added the `course_reqs` array. - - - `prefix_regex::Regex` : regular expression for matching a course prefix in the course catalog. Default is `".*"`, - - i.e., match any character any number of times. - - - `num_regex::Regex` : regular expression for matching a course number in the course catalog. Default is `".*"`, - - i.e., match any character any number of times. - - - `min_grade::Grade` : The minimum letter grade that must be earned in courses satisfying the regular expressions. - - - `double_count::Bool` : Specifies whether or not each course in the course set can be used to satisfy other requirements - - that contain any of the courses in this `CourseSet`. Default = false - - - - # Examples: - - ```julia-repl - - julia> CourseSet("General Education - Mathematics", 9, courses) - - ``` - - where `courses` is an array of Course=>Grade `Pairs`, i.e., the set of courses/minimum grades that can satsfy this - - degree requirement. - - """ - - mutable struct CourseSet <: AbstractRequirement - - id::Int # Unique requirement id - - name::AbstractString # Name of the requirement (must be unique) - - description::AbstractString # Requirement description - - credit_hours::Real # The number of credit hours required to satisfy the requirement - - course_reqs::Array{Pair{Course, Grade}} # The courses that the required credit hours may be drawn from, and the minimum grade that must be earned in each - - course_catalog::CourseCatalog # Course catalog to draw courses from using the following regular expressions (positive matches are stored in course_reqs) - - prefix_regex::Regex # Regular expression for matching a course prefix in the course catalog. - - num_regex::Regex # Regular expression for matching a course number in the course catalog, must satisfy both - - min_grade::Grade # The minimum letter grade that must be earned in courses satisfying the regular expressions - - double_count::Bool # Each course in the course set can satisfy any other requirement that has the same course. Default = false - - - - # Constructor - - # A requirement may involve a set of courses, or a set of requirements, but not both - 26 function CourseSet(name::AbstractString, credit_hours::Real, course_reqs::Array{Pair{Course,Grade},1}=Array{Pair{Course,Grade},1}(); description::AbstractString="", - - course_catalog::CourseCatalog=CourseCatalog("", ""), prefix_regex::Regex=r".^", num_regex::Regex=r".^", course_regex::Regex=r".^", - - min_grade::Grade=grade("D"), double_count::Bool=false) - - # r".^" is a regex that matches nothing - 26 this = new() - 13 this.name = name - 13 this.description = description - 13 this.credit_hours = credit_hours - 13 this.id = mod(hash(this.name * this.description * string(this.credit_hours)), UInt32) - 13 this.course_reqs = course_reqs - 13 this.course_catalog = course_catalog - 13 this.prefix_regex = prefix_regex - 13 this.num_regex = num_regex - 13 this.double_count = double_count - 15 for c in course_catalog.catalog # search the supplied course catalog for courses satisfying both prefix and num regular expressions - 14 if occursin(prefix_regex, c[2].prefix) && occursin(num_regex, c[2].num) - 14 push!(course_reqs, c[2] => min_grade) - - end - - end - - #if this.credit_hours > sum of course credits - - # ## TODO: add this warning if credits are not sufficient - - #end - 13 sum = 0 - 33 sorted = sort(this.course_reqs; by = x -> x.first.credit_hours, rev = true) - 13 for c in sorted - 20 sum += c.first.credit_hours - 28 sum >= this.credit_hours ? break : nothing - - end - 13 if (sum - this.credit_hours) < 0 # credits provided by courses are not sufficent to satisfy required number of credit hours for this requirement - 1 printstyled("WARNING: Course set $(this.name) is improperly specified, $(this.credit_hours) credits are required, but credits amounting to $sum are available.\nUse is_valid() to check a requirement set for specification errors.\n", color = :yellow) - - end - - - 13 return this - - end - - end - - - - """ - - The `RequirementSet` data type is used to represent a collection of requirements. To instantiate a `RequirementSet` use: - - - - RequirementSet(name, credit_hours, requirements, description) - - - - # Arguments - - Required: - - - `name::AbstractString` : the name of the degree requirement set. - - - `credit_hours::Real` : the number of credit hours required in order to satisfy the requirement set. - - - `requirements::Array{AbstractRequirement}` : the course sets or requirement sets that comprise the requirement set. - - Keyword: - - - `description::AbstractString` : the description of the requirement set. - - - `satisfy::Int` : the number of requirements in the set that must be satisfied. Default is all. - - - - # Examples: - - ```julia-repl - - julia> RequirementSet("General Education Core", 30, requirements) - - ``` - - where `requirements` is an array of `CourseSet` and/or `RequirementSet` objects. - - """ - - mutable struct RequirementSet <: AbstractRequirement - - id::Int # Unique requirement id (internally generated) - - name::AbstractString # Name of the requirement (must be unique) - - description::AbstractString # Requirement description - - credit_hours::Real # The number of credit hours required to satisfy the requirement - - requirements::Array{AbstractRequirement} # The set of requirements (course sets or requirements sets) that define this requirement - - satisfy::Int # The number of requirements in the set that must be satisfied. Default is all. - - - - # Constructor - 14 function RequirementSet(name::AbstractString, credit_hours::Real, requirements::Array{T,1}; - - description::AbstractString="", satisfy::Int=0) where T <: AbstractRequirement - 14 this = new() - 7 this.name = name - 7 this.description = description - 7 this.credit_hours = credit_hours - 7 this.id = mod(hash(this.name * this.description * string(this.credit_hours)), UInt32) - 7 this.requirements = requirements - 7 if satisfy < 0 - 0 printstyled("WARNING: RequirementSet $(this.name), satisfy cannot be a negative number\n", color = :yellow) - 7 elseif satisfy == 0 - 4 this.satisfy = length(requirements) # satisfy all requirements - 3 elseif satisfy <= length(requirements) - 3 this.satisfy = satisfy - - else # satisfy > length(requirements) - 0 this.satisfy = satisfy - - # trying to satisfy more then the # of available sub-requirements - 0 printstyled("WARNING: RequirementSet $(this.name), satisfy variable ($(this.satisfy)) cannot be greater than the number of available requirements ($(length(this.requirements)))\n", color = :yellow) - - end - 7 return this - - end - - end - - - - """ - - Determine whether or not a set of requirements contained in a requirements tree rooted at `root` has credit hour - - constraints that are possible to satisfy. - - - - is_valid(root::RequirementSet, errors::IOBuffer) - - - - ```julia-repl - - julia> errors = IOBuffer() - - julia> is_valid(root, errors) - - julia> println(String(take!(errors))) - - ``` - - - - The credit hour constraints associated with particular set of requirements may be specified in a way that - - makes them impossible to satsify. This function searches for particular cases of this problem, and if found, - - reports them in an error message. - - """ - 4 function is_valid(root::AbstractRequirement, error_msg::IOBuffer = IOBuffer()) - 4 validity = true - 4 reqs = preorder_traversal(root) - 4 dups = nonunique(reqs) - 4 if (length(dups) > 0) # the same requirements is being used multiple times, so it's not a requirments tree - 0 validity = false - 0 write(error_msg, "RequirementSet: $(root.name) is not a tree, it contains duplicate requirements: \n") - 0 for d in dups - 0 write(error_msg,"\t $(d.name)\n") - - end - - end - 4 for r in reqs - 31 credit_total = 0 - 31 if typeof(r) == CourseSet - 24 for c in r.course_reqs - 42 credit_total += c[1].credit_hours - - end - - # credit_total = sum(sum(x)->x, ... ) # make above code more compact using map-reduce functionality - 24 if r.credit_hours > credit_total - 1 validity = false - 1 write(error_msg, "CourseSet: $(r.name) is unsatisfiable,\n\t $(r.credit_hours) credits are required from courses having only $(credit_total) credit hours.\n") - - end - - else # r is a RequirementSet - 7 if (r.satisfy == 0) - 0 validity = false - 0 write(error_msg, "RequirementSet: $(r.name) is unsatisfiable, cannot satisfy 0 requirments\n") - 7 elseif (r.satisfy > length(r.requirements)) - 0 validity = false - 0 write(error_msg, "RequirementSet: $(r.name) is unsatisfiable, satisfy variable cannot be greater than the number of available requirements\n") - - else # r.satisy <= number of requirements - 7 credit_ary = [] - 7 for child in r.requirements - 27 push!(credit_ary, child.credit_hours) - - end - 7 max_credits = 0 - 14 for c in combinations(credit_ary, r.satisfy) # find the max. credits possible from r.satisfy number of requirements - 11 sum(c) > max_credits ? max_credits = sum(c) : nothing - - end - 7 if r.credit_hours > max_credits - 1 validity = false - 36 write(error_msg, "RequirementSet: $(r.name) is unsatisfiable,\n\t $(r.credit_hours) credits are required from sub-requirements that can provide at most $(max_credits) credit hours.\n") - - end - - end - - end - - end - 4 return validity - - end - - - - # helper function for finding the duplicate elements in an array - 4 function nonunique(x::AbstractArray{T}) where T - 4 uniqueset = Set{T}() - 4 duplicateset = Set{T}() - 4 for i in x - 31 if (i in uniqueset) - 0 push!(duplicateset, i) - - else - 35 push!(uniqueset, i) - - end - - end - 4 collect(duplicateset) - - end diff --git a/src/DataTypes/Simulation.jl.33665.cov b/src/DataTypes/Simulation.jl.33665.cov deleted file mode 100644 index c16267c..0000000 --- a/src/DataTypes/Simulation.jl.33665.cov +++ /dev/null @@ -1,50 +0,0 @@ - - mutable struct Simulation - - degree_plan::DegreePlan # The curriculum that is simulated - - duration::Int # The number of terms the simulation runs for - - course_attempt_limit::Int # The number of times that a course is allowed to take - - - - prediction_model::Module # Module that implements the model for predicting student's performance in courses - - - - num_students::Int # The number of students in the simulation - - enrolled_students::Array{Student} # Array of students that are enrolled - - graduated_students::Array{Student} # Array of students that have graduated - - stopout_students::Array{Student} # Array of students who stopped out - - - - reach_attempts_students::Array{Student} # Array of students who have reached max course attempts - - reach_attempts_rates::Array{Float64} # Array of student reaching max course attemps rates - - - - student_progress::Array{Int} # Indicates wheter students have passed each course - - student_attemps::Array{Int} # Number of attemps that students have taken for each course - - - - grad_rate::Float64 # Graduation rate at the end of the simulation - - term_grad_rates::Array{Float64} # Array of graduation rates at the end of the simulation - - time_to_degree::Float64 # Average number of semesters it takes to graduate students - - stopout_rate::Float64 # Stopout rate at the end of the simulation - - term_stopout_rates::Array{Float64} # Array of stopout rates for each term - - - 3 function Simulation(degree_plan) - 3 this = new() - - - 3 this.degree_plan = degree_plan - 3 this.enrolled_students = Student[] - 3 this.graduated_students = Student[] - 3 this.stopout_students = Student[] - - - - # Set up degree plan - 3 degree_plan.metadata["stopout_model"] = Dict() - - - - # Set up courses - 3 for (id, course) in enumerate(degree_plan.curriculum.courses) - 24 course.metadata["id"] = id - 24 course.metadata["failures"] = 0 - 24 course.metadata["enrolled"] = 0 - 24 course.metadata["passrate"] = 0 - 24 course.metadata["term_req"] = 0 - 24 course.metadata["grades"] = Float64[] - 24 course.metadata["students"] = Student[] - 24 course.metadata["model"] = Dict() - - end - - - 3 return this - - end - - end diff --git a/src/DataTypes/Student.jl.33665.cov b/src/DataTypes/Student.jl.33665.cov deleted file mode 100644 index 418d66e..0000000 --- a/src/DataTypes/Student.jl.33665.cov +++ /dev/null @@ -1,41 +0,0 @@ - - mutable struct Student - - id::Int # Unique ID for student - - total_credits::Int # The total number of credit hours the student has earned - - gpa::Float64 # The student's GPA - - total_points::Float64 # The total number of points the student has earned - - - - attributes::Dict # A dictionary that can store any kind of student attribute - - stopout::Bool # Indicates whether the student has stopped out. (False if the student has, True if still enrolled) - - stopsem::Bool # The term the student stopped out. - - termcredits::Int # The number of credits the student has enrolled in for a given term. - - performance::Dict # Stores the grades the student has made in each course. - - graduated::Bool # Indicates wheter the student has graduated. - - gradsem::Int # The term the student has graduated. - - termpassed::Array{Int} # An array that represents the term in which the student passed each course. - - - - - - # Constructor - 242 function Student(id::Int; attributes::Dict=Dict()) - 242 this = new() - 121 this.id = id - 121 this.termcredits = 0 - 121 this.performance = Dict() - 121 this.gpa = 0.0 - 121 this.total_credits = 0 - 121 this.total_points = 0 - 121 this.attributes = attributes - - - 121 return this - - end - - end - - - - # Returns an array of students - 3 function simple_students(number) - 3 students = Student[] - 6 for i = 1:number - 120 student = Student(i) - 120 student.stopout = false - 237 push!(students, student) - - end - 3 return students - - end diff --git a/src/DataTypes/StudentRecord.jl.33665.cov b/src/DataTypes/StudentRecord.jl.33665.cov deleted file mode 100644 index 6d13659..0000000 --- a/src/DataTypes/StudentRecord.jl.33665.cov +++ /dev/null @@ -1,37 +0,0 @@ - - - - # Course record - record of performance in a single course - - mutable struct CourseRecord - - course::Course # course that was attempted - - grade::Grade # grade earned in the course - - term::AbstractString # term course was attempted - - - - # Constructor - 2 function CourseRecord(course::Course, grade::Grade, term::AbstractString="") - 2 this = new() - 2 this.course = course - 2 this.grade = grade - 2 this.term = term - 2 return this - - end - - end - - - - # Student record data type, i.e., a transcript - - mutable struct StudentRecord - - id::AbstractString # unique student id - - first_name::AbstractString # student's first name - - last_name::AbstractString # student's last name - - middle_initial::AbstractString # Student's middle initial or name - - transcript::Array{CourseRecord} # list of student grades - - GPA::Real # student's GPA - - - - # Constructor - 1 function StudentRecord(id::AbstractString, first_name::AbstractString, last_name::AbstractString, middle_initial::AbstractString, - - transcript::Array{CourseRecord,1}) - 1 this = new() - 1 this.first_name = first_name - 1 this.last_name = last_name - 1 this.middle_initial = middle_initial - 1 this.transcript = transcript - 1 return this - - end - - end diff --git a/src/DataTypes/TransferArticulation.jl.33665.cov b/src/DataTypes/TransferArticulation.jl.33665.cov deleted file mode 100644 index 27fbfae..0000000 --- a/src/DataTypes/TransferArticulation.jl.33665.cov +++ /dev/null @@ -1,42 +0,0 @@ - - - - # Transfer articulation map for a home (recieving xfer) institution - - mutable struct TransferArticulation - - name::AbstractString # Name of the transfer articulation data structure - - institution::AbstractString # Institution receiving the transfer courses (home institution) - - date_range::Tuple # Range of dates over which the transfer articulation data is applicable - - transfer_catalogs::Dict{Int,CourseCatalog} # Dictionary of transfer institution catalogs, in (CourseCatalog id, catalog) format - - home_catalog::CourseCatalog # Course catalog of recieving institution - - transfer_map::Dict{Tuple{Int,Int},Array{Int}} # Dictionary in ((xfer_catalog_id, xfer_course_id), array of home_course_ids) format - - - - # Constructor - 3 function TransferArticulation(name::AbstractString, institution::AbstractString, home_catalog::CourseCatalog, - - transfer_catalogs::Dict{Int,CourseCatalog}=Dict{Int,CourseCatalog}(), - - transfer_map::Dict{Tuple{Int,Int},Array{Int}}=Dict{Tuple{Int,Int},Array{Int}}(), date_range::Tuple=()) - 3 this = new() - 1 this.name = name - 1 this.institution = institution - 1 this.transfer_catalogs = transfer_catalogs - 1 this.home_catalog = home_catalog - 1 this.transfer_map = transfer_map - 1 return this - - end - - end - - - 1 function add_transfer_catalog(ta::TransferArticulation, transfer_catalog::CourseCatalog) - 1 ta.transfer_catalogs[transfer_catalog.id] = transfer_catalog - - end - - - - # A single transfer course may articulate to more than one course at the home institution - - # TODO: add the ability for a transfer course to partially satifsfy a home institution course (i.e., some, but not all, course credits) - 2 function add_transfer_course(ta::TransferArticulation, home_course_ids::Array{Int}, transfer_catalog_id::Int, transfer_course_id::Int) - 2 ta.transfer_map[(transfer_catalog_id, transfer_course_id)] = [] - 2 for id in home_course_ids - 2 push!(ta.transfer_map[(transfer_catalog_id, transfer_course_id)], id) - - end - - end - - - - # Find the course equivalency at a home institution of a course being transfered from another institution - - # returns transfer equivalent course, or nothing if there is no transfer equivalency - 2 function transfer_equiv(ta::TransferArticulation, transfer_catalog_id::Int, transfer_course_id::Int) - 2 haskey(ta.transfer_map, (transfer_catalog_id, transfer_course_id)) ? ta.transfer_map[(transfer_catalog_id, transfer_course_id)] : nothing - - end diff --git a/src/DegreePlanAnalytics.jl.33665.cov b/src/DegreePlanAnalytics.jl.33665.cov deleted file mode 100644 index 43860a9..0000000 --- a/src/DegreePlanAnalytics.jl.33665.cov +++ /dev/null @@ -1,141 +0,0 @@ - - #File: DegreePlanAnalytics.jl - - - - # Basic metrics for a degree plan, based soley on credits - - """ - - basic_metrics(plan) - - - - Compute the basic metrics associated with degree plan `plan`, and return an IO buffer containing these metrics. The baseic - - metrics are primarily concerned with how credits hours are distributed across the terms in a plan. The basic metrics are - - also stored in the `metrics` dictionary associated with the degree plan. - - - - # Arguments - - Required: - - - `plan::DegreePlan` : a valid degree plan (see [Degree Plans](@ref)). - - - - The basic metrics computed include: - - - - - number of terms : The total number of terms (semesters or quarters) in the degree plan, ``m``. - - - total credit hours : The total number of credit hours in the degree plan. - - - max. credits in a term : The maximum number of credit hours in any one term in the degree plan. - - - min. credits in a term : The minimum number of credit hours in any one term in the degree plan. - - - max. credit term : The earliest term in the degree plan that has the maximum number of credit hours. - - - min. credit term : The earliest term in the degree plan that has the minimum number of credit hours. - - - avg. credits per term : The average number of credit hours per term in the degree plan, ``\\overline{ch}``. - - - term credit hour std. dev. : The standard deviation of credit hours across all terms ``\\sigma``. If ``ch_i`` denotes the number - - of credit hours in term ``i``, then - - - - ```math - - \\sigma = \\sqrt{\\sum_{i=1}^m {(ch_i - \\overline{ch})^2 \\over m}} - - ``` - - - - To view the basic degree plan metrics associated with degree plan `plan` in the Julia console use: - - - - ```julia-repl - - julia> metrics = basic_metrics(plan) - - julia> println(String(take!(metrics))) - - julia> # The metrics are also stored in a dictonary that can be accessed as follows - - julia> plan.metrics - - ``` - - """ - 1 function basic_metrics(plan::DegreePlan) - 1 buf = IOBuffer() - 1 write(buf, "\nCurriculum: $(plan.curriculum.name)\nDegree Plan: $(plan.name)\n") - 1 plan.metrics["total credit hours"] = plan.credit_hours - 1 write(buf, " total credit hours = $(plan.credit_hours)\n") - 1 plan.metrics["number of terms"] = plan.num_terms - 1 write(buf, " number of terms = $(plan.num_terms)\n") - 1 max = 0 - 1 min = plan.credit_hours - 1 max_term = 0 - 1 min_term = 0 - 1 var = 0 - 1 req_distance = 0 - 1 avg = plan.credit_hours/plan.num_terms - 2 for i = 1:plan.num_terms - 3 if plan.terms[i].credit_hours > max - 2 max = plan.terms[i].credit_hours - 2 max_term = i - - end - 3 if plan.terms[i].credit_hours < min - 2 min = plan.terms[i].credit_hours - 2 min_term = i - - end - 5 var = var + (plan.terms[i].credit_hours - avg)^2 - - end - 1 plan.metrics["max. credits in a term"] = max - 1 plan.metrics["max. credit term"] = max_term - 1 write(buf, " max. credits in a term = $(max), in term $(max_term)\n") - 1 plan.metrics["min. credits in a term"] = min - 1 plan.metrics["min. credit term"] = min_term - 1 write(buf, " min. credits in a term = $(min), in term $(min_term)\n") - 1 plan.metrics["avg. credits per term"] = avg - 1 plan.metrics["term credit hour std. dev."] = sqrt(var/plan.num_terms) - 1 write(buf, " avg. credits per term = $(avg), with std. dev. = $(plan.metrics["term credit hour std. dev."])\n") - 1 return buf - - end - - - - # Degree plan metrics based upon the distance between requsites and the classes that require them. - - """ - - requisite_distance(DegreePlan, course::Course) - - - - For a given degree plan `plan` and target course `course`, this function computes the total distance in the degree plan between `course` and - - all of its requisite courses. - - - - # Arguments - - Required: - - - `plan::DegreePlan` : a valid degree plan (see [Degree Plans](@ref)). - - - `course::Course` : the target course. - - - - The distance between a target course and one of its requisites is given by the number of terms that separate the target - - course from that particular requisite in the degree plan. To compute the requisite distance, we sum this distance over all - - requisites. That is, if write let ``T_j^p`` denote the term in degree plan ``p`` that course ``c_j`` appears in, then for a - - degree plan with underlying curriculum graph ``G_c = (V,E)``, the requisite distance for course ``c_j`` in degree plan ``p``, - - denoted ``rd_{v_j}^p``, is: - - - - ```math - - rd_{v_j}^p = \\sum_{\\{i | (v_i, v_j) \\in E\\}} (T_j - T_i). - - ``` - - - - In general, it is desirable for a course and its requisites to appear as close together as possible in a degree plan. - - The requisite distance metric computed by this function is stored in the associated `Course` data object. - - """ - 12 function requisite_distance(plan::DegreePlan, course::Course) - 12 distance = 0 - 12 term = find_term(plan, course) - 24 for req in keys(course.requisites) - 24 distance = distance + (term - find_term(plan, course_from_id(plan.curriculum, req))) - - end - 12 return course.metrics["requisite distance"] = distance - - end - - - - """ - - requisite_distance(plan::DegreePlan) - - - - For a given degree plan `plan`, this function computes the total distance between all courses in the degree plan, and - - the requisites for those courses. - - - - # Arguments - - Required: - - - `plan::DegreePlan` : a valid degree plan (see [Degree Plans](@ref)). - - - - The distance between a course a requisite is given by the number of terms that separate the course from - - its requisite in the degree plan. If ``rd_{v_i}^p`` denotes the requisite distance between course - - ``c_i`` and its requisites in degree plan ``p``, then the total requisite distance for a degree plan, - - denoted ``rd^p``, is given by: - - - - ```math - - rd^p = \\sum_{v_i \\in V} = rd_{v_i}^p - - ``` - - - - In general, it is desirable for a course and its requisites to appear as close together as possible in a degree plan. - - Thus, a degree plan that minimizes these distances is desirable. A optimization function that minimizes requisite - - distances across all courses in a degree plan is described in [Optimized Degree Plans]@ref. - - The requisite distance metric computed by this function will be stored in the associated `DegreePlan` data object. - - """ - 1 function requisite_distance(plan::DegreePlan) - 1 distance = 0 - 1 for c in plan.curriculum.courses - 7 distance = distance + requisite_distance(plan, c) - - end - 1 return plan.metrics["requisite distance"] = distance - - end diff --git a/src/DegreePlanCreation.jl.33665.cov b/src/DegreePlanCreation.jl.33665.cov deleted file mode 100644 index f610327..0000000 --- a/src/DegreePlanCreation.jl.33665.cov +++ /dev/null @@ -1,84 +0,0 @@ - - # file: DegreePlanCreation.jl - - - 5 function create_degree_plan(curric::Curriculum, create_terms::Function=bin_filling, name::AbstractString="", additional_courses::Array{AbstractCourse}=Array{AbstractCourse,1}(); - - min_terms::Int=1, max_terms::Int=10, min_cpt::Int=3, max_cpt::Int=19) - 5 terms = create_terms(curric, additional_courses; min_terms=min_terms, max_terms=max_terms, min_cpt=min_cpt, max_cpt=max_cpt) - 1 if terms == false - 0 println("Unable to create degree plan") - 0 return - - else - 1 return DegreePlan(name, curric, terms) - - end - - end - - - 8 function bin_filling(curric::Curriculum, additional_courses::Array{AbstractCourse}=Array{AbstractCourse,1}(); min_terms::Int=2, max_terms::Int=10, min_cpt::Int=3, max_cpt::Int=19) - 8 terms = Array{Term,1}() - 3 term_credits = 0 - 3 term_courses = Array{AbstractCourse,1}() - 3 UC = sort!(deepcopy(curric.courses), by=course_num) # lower numbered courses will be considered first - 12 while length(UC) > 0 - 17 if ((c = select_vertex(curric, term_courses, UC)) != nothing) - 16 deleteat!(UC, findfirst(isequal(c), UC)) - 8 if term_credits + c.credit_hours <= max_cpt - 12 append!(term_courses, [c]) - 6 term_credits = term_credits + c.credit_hours - - else # exceeded max credits allowed per term - 2 append!(terms, [Term(term_courses)]) - 2 term_courses = AbstractCourse[c] - 2 term_credits = c.credit_hours - - end - - # if c serves as a strict-corequisite for other courses, include them in current term too - 9 for course in UC - 12 for req in course.requisites - 9 if req[1] == c.id - 6 if req[2] == strict_co - 8 deleteat!(UC, findfirst(isequal(course), UC)) - 4 append!(term_courses, [course]) - 11 term_credits = term_credits + course.credit_hours - - end - - end - - end - - end - - else # can't find a course to add to current term, create a new term - 1 length(term_courses) > 0 ? append!(terms, [Term(term_courses)]) : nothing - 1 term_courses = AbstractCourse[] - 10 term_credits = 0 - - end - - end - 3 length(term_courses) > 0 ? append!(terms, [Term(term_courses)]) : nothing - 3 return terms - - end - - - 9 function select_vertex(curric::Curriculum, term_courses::Array{AbstractCourse,1}, UC::Array{AbstractCourse,1}) - 9 for target in UC - 10 t_id = target.vertex_id[curric.id] - 10 UCs = deepcopy(UC) - 31 deleteat!(UCs, findfirst(c->c.id==target.id, UCs)) - 10 invariant1 = true - 11 for source in UCs - 16 s_id = source.vertex_id[curric.id] - 16 vlist = reachable_from(curric.graph, s_id) - 16 if t_id in vlist # target cannot be moved to AC - 0 invariant1 = false # invariant 1 violated - 16 break # try a new target - - end - - end - 10 if invariant1 == true - 10 invariant2 = true - 14 for c in term_courses - 9 if c.id in collect(keys(target.requisites)) && target.requisites[c.id] == pre # AND shortcircuits, otherwise 2nd expression would error - 2 invariant2 = false - 9 break # try a new target - - end - - end - 10 if invariant2 == true - 9 return target - - end - - end - - end - 1 return nothing - - end - - - 36 function course_num(c::Course) - 36 c.num != "" ? c.num : c.name - - end diff --git a/src/GraphAlgs.jl.33665.cov b/src/GraphAlgs.jl.33665.cov deleted file mode 100644 index 993242a..0000000 --- a/src/GraphAlgs.jl.33665.cov +++ /dev/null @@ -1,381 +0,0 @@ - - # File: GraphAlgs.jl - - - - # Depth-first search, returns edge classification using EdgeClass, as well as the discovery and finish time for each vertex. - - """ - - dfs(g) - - - - Perform a depth-first traversal of input graph `g`. - - - - # Arguments - - Required: - - - `g::AbstractGraph` : input graph. - - - - This function returns the classification of each edge in graph `g`, as well as the order in which vertices are - - first discovered during a depth-first search traversal, and when the processing from that vertex is completed - - during the depth-first traverlsa. According to the vertex discovery and finish times, each edge in `g` will be - - classified as one of: - - - *tree edge* : Any collection of edges in `g` that form a forest. Every vertex is either a single-vertex tree - - with respect to such a collection, or is part of some larger tree through its connection to another vertex via a - - tree edge. This collection is not unique defined on `g`. - - - *back edge* : Given a collection of tree edges, back edges are those edges that connect some descendent vertex - - in a tree to an ancestor vertex in the same tree. - - - *forward edge* : Given a collection of tree edges, forward edges are those that are incident from an ancestor - - in a tree, and incident to an descendent in the same tree. - - - *cross edge* : Given a collection of tree edges, cross edges are those that are adjacent between vertices in - - two different trees, or between vertices in two different subtrees in the same tree. - - - - ```julia-repl - - julia> edges, discover, finish = dfs(g) - - ``` - - """ - 4 function dfs(g::AbstractGraph{T}) where T - 4 time = 0 - - # discover and finish times - 48 d = zeros(Int, nv(g)) - 48 f = zeros(Int, nv(g)) - 4 edge_type = Dict{Edge,EdgeClass}() - 8 for s in vertices(g) - 48 if d[s] == 0 # undiscovered - - # a closure, shares variable space w/ outer function - 72 function dfs_visit(s) - 48 d[s] = time += 1 # discovered - 80 for n in neighbors(g, s) - 28 if d[n] == 0 # encounted a undiscovered vertex - 24 edge_type[Edge(s,n)] = tree_edge - 24 dfs_visit(n) - 4 elseif f[n] == 0 # encountered a discovered but unfinished vertex - 0 edge_type[Edge(s,n)] = back_edge - - else # encountered a finished vertex - 4 if d[s] < d[n] - 0 edge_type[Edge(s,n)] = forward_edge - - else # d[s] > d[n] - 48 edge_type[Edge(s,n)] = cross_edge - - end - - end - - end - 48 f[s] = time += 1 # finished - - end # end closure - 68 dfs_visit(s) # call the closure - - end - - end - 4 return edge_type, d, f - - end # end dfs - - - - # In a DFS of a DAG, sorting the vertices according to their finish times in the DFS will yeild a topological sorting of the - - # DAG vertices. - - """ - - topological_sort(g; ) - - - - Perform a topoloical sort on graph `g`, returning the weakly connected components of the graph, each in topological sort order. - - If the `sort` keyword agrument is supplied, the components will be sorted according to their size, in either ascending or - - descending order. If two or more components have the same size, the one with the smallest vertex ID in the first position of the - - topological sort will appear first. - - - - # Arguments - - Required: - - - `g::AbstractGraph` : input graph. - - - - Keyword: - - - `sort::String` : sort weakly connected components according to their size, allowable - - strings: `ascending`, `descending`. - - """ - 6 function topological_sort(g::AbstractGraph{T}; sort::String="") where T - 6 edges_type, d, f = dfs(g) - 3 topo_order = sortperm(f, rev=true) - 3 wcc = weakly_connected_components(g) - 3 if sort == "descending" - 6 sort!(wcc, lt = (x,y) -> size(x) != size(y) ? size(x) > size(y) : x[1] < y[1]) # order components by size, if same size, by lower index - 2 elseif sort == "ascending" - 10 sort!(wcc, lt = (x,y) -> size(x) != size(y) ? size(x) < size(y) : x[1] < y[1]) # order components by size, if same size, by lower index - - end - 3 reorder = [] - 3 for component in wcc - 57 sort!(component, lt = (x,y) -> indexin(x, topo_order)[1] < indexin(y, topo_order)[1]) # topological sort within each component - 15 for i in component - 39 push!(reorder, i) # add verteix indicies to the end of the reorder array - - end - - end - 3 return wcc - - end - - - - # transpose of DAG - - """ - - gad(g) - - - - Returns the transpose of directed acyclic graph (DAG) `g`, i.e., a DAG identical to `g`, except the direction - - of all edges is reversed. If `g` is not a DAG, and error is thrown. - - - - # Arguments - - Required: - - - `g::SimpleDiGraph` : input graph. - - """ - 5 function gad(g::AbstractGraph{T}) where T - 5 @assert typeof(g) == SimpleDiGraph{T} - 5 return SimpleDiGraph(transpose(adjacency_matrix(g))) - - end - - - - # The set of all vertices in the graph reachable from vertex s - - """ - - reachable_from(g, s) - - - - Returns the the set of all vertices in `g` that are reachable from vertex `s`. - - - - # Arguments - - Required: - - - `g::AbstractGraph` : acylic input graph. - - - `s::Int` : index of the source vertex in `g`. - - """ - 157 function reachable_from(g::AbstractGraph{T}, s::Int, vlist::Array=Array{Int64,1}()) where T - 222 for v in neighbors(g, s) - 71 if findfirst(isequal(v), vlist) == nothing # v is not in vlist - 55 push!(vlist, v) - - end - 108 reachable_from(g, v, vlist) - - end - 110 return vlist - - end - - - - # The subgraph induced by vertex s and the vertices reachable from vertex s - - """ - - reachable_from_subgraph(g, s) - - - - Returns the subgraph induced by `s` in `g` (i.e., a graph object consisting of vertex - - `s` and all vertices reachable from vertex `s` in`g`), as well as a vector mapping the vertex - - IDs in the subgraph to their IDs in the orginal graph `g`. - - - - ```julia-rep - - sg, vmap = reachable_from_subgraph(g, s) - - ```` - - """ - 1 function reachable_from_subgraph(g::AbstractGraph{T}, s::Int) where T - 1 vertices = reachable_from(g, s) - 1 push!(vertices, s) # add the source vertex to the reachable set - 1 induced_subgraph(g, vertices) - - end - - - - # The set of all vertices in the graph that can reach vertex s - - """ - - reachable_to(g, t) - - - - Returns the set of all vertices in `g` that can reach target vertex `t` through any path. - - - - # Arguments - - Required: - - - `g::AbstractGraph` : acylic input graph. - - - `t::Int` : index of the target vertex in `g`. - - """ - 4 function reachable_to(g::AbstractGraph{T}, t::Int) where T - 4 reachable_from(gad(g), t) # vertices reachable from s in the transpose graph - - end - - - - # The subgraph induced by vertex s and the vertices that can reach s - - """ - - reachable_to_subgraph(g, t) - - - - Returns a subgraph in `g` consisting of vertex `t` and all vertices that can reach - - vertex `t` in`g`, as well as a vector mapping the vertex IDs in the subgraph to their IDs - - in the orginal graph `g`. - - - - # Arguments - - Required: - - - `g::AbstractGraph` : acylic graph. - - - `t::Int` : index of the target vertex in `g`. - - - - ```julia-rep - - sg, vmap = reachable_to(g, t) - - ```` - - """ - 1 function reachable_to_subgraph(g::AbstractGraph{T}, s::Int) where T - 1 vertices = reachable_to(g, s) - 1 push!(vertices, s) # add the source vertex to the reachable set - 1 induced_subgraph(g, vertices) - - end - - - - # The set of all vertices reachable to and reachable from vertex s - - """ - - reach(g, v) - - - - Returns the reach of vertex `v` in `g`, ie., the set of all vertices in `g` that can - - reach vertex `v` and can be reached from `v`. - - - - # Arguments - - Required: - - - `g::AbstractGraph` : acylic graph. - - - `v::Int` : index of a vertex in `g`. - - """ - 2 function reach(g::AbstractGraph{T}, v::Int) where T - 2 union(reachable_to(g, v), reachable_from(g, v)) - - end - - - - # Subgraph induced by the reach of a vertex - - """ - - reach_subgraph(g, v) - - - - Returns a subgraph in `g` consisting of vertex `v ` and all vertices that can reach `v`, as - - well as all vertices that `v` can reach. In addition, a vector is returned that maps the - - vertex IDs in the subgraph to their IDs in the orginal graph `g`. - - - - # Arguments - - Required: - - - `g::AbstractGraph` : acylic graph. - - - `v::Int` : index of a vertex in `g`. - - - - ```julia-rep - - sg, vmap = reachable_to(g, v) - - ```` - - """ - 1 function reach_subgraph(g::AbstractGraph{T}, v::Int) where T - 1 vertices = reach(g, v) - 1 push!(vertices, v) # add the source vertex to the reachable set - 1 induced_subgraph(g, vertices) - - end - - - - # find all paths in a graph - - """ - - all_paths(g) - - - - Enumerate all of the unique paths in acyclic graph `g`, where a path in this case must include a - - source vertex (a vertex with in-degree zero) and a different sink vertex (a vertex with out-degree - - zero). I.e., a path is this case must contain at least two vertices. This function returns - - an array of these paths, where each path consists of an array of vertex IDs. - - - - # Arguments - - Required: - - - `g::AbstractGraph` : acylic graph. - - - - ```julia-repl - - julia> paths = all_paths(g) - - ``` - - """ - 55 function all_paths(g::AbstractGraph{T}) where T - - # check that g is acyclic - 55 if is_cyclic(g) - 0 error("all_paths(): input graph has cycles") - - end - 55 que = Queue{Array}() - 55 paths = Array[] - 55 sinks = Int[] - 110 for v in vertices(g) - 420 if (length(outneighbors(g,v)) == 0) && (length(inneighbors(g,v)) > 0) # consider only sink vertices with a non-zero in-degree - 478 push!(sinks, v) - - end - - end - 55 for v in sinks - 113 enqueue!(que, [v]) - 389 while !isempty(que) # work backwards from sink v to all sources reachable to v in BFS fashion - 276 x = dequeue!(que) # grab a path from the queue - 552 for (i, u) in enumerate(inneighbors(g, x[1])) # consider the in-neighbors at the beginning of the current path - 392 if i == 1 # first neighbor, build onto exising path - 276 insert!(x, 1, u) # prepend vertex u to array x - - # if reached a source vertex, done with the path; otherwise, put it back in queue - 393 length(inneighbors(g, u)) == 0 ? push!(paths, x) : enqueue!(que, x) - - else # not first neighbor, create a copy of the path - 116 y = copy(x) - 116 y[1] = u # overwrite first element in array - 676 length(inneighbors(g, u)) == 0 ? push!(paths, y) : enqueue!(que, y) - - end - - end - - end - - end - 55 return paths - - end - - - - # The longest path from vertx s to any other vertex in a DAG G (not necessarily unique). - - # Note: in a DAG G, longest paths in G = shortest paths in -G - - """ - - longest_path(g, s) - - - - The longest path from vertx s to any other vertex in a acyclic graph `g`. The longest path - - is not necessarily unique, i.e., there can be more than one longest path between two vertices. - - - - # Arguments - - Required: - - - `g::AbstractGraph` : acylic graph. - - - `s::Int` : index of the source vertex in `g`. - - - - ```julia-repl - - julia> path = longest_paths(g, s) - - ``` - - """ - 1 function longest_path(g::AbstractGraph{T}, s::Int) where T - 1 if is_cyclic(g) - 0 error("longest_path(): input graph has cycles") - - end - 1 lp = Array{Edge}[] - 1 max = 0 - - # shortest path from s to all vertices in -G - 1 for path in enumerate_paths(dijkstra_shortest_paths(g, s, -weights(g))) - 12 if length(path) > max - 2 lp = path - 3 max = length(path) - - end - - end - 1 return lp - - end - - - - # Find all of the longest paths in an acyclic graph. - - """ - - longest_paths(g) - - - - Finds the set of longest paths in `g`, and returns an array of vertex arrays, where each vertex - - array contains the vertices in a longest path. - - - - # Arguments - - Required: - - - `g::AbstractGraph` : acylic graph. - - - - ```julia-repl - - julia> paths = longest_paths(g) - - ``` - - """ - 2 function longest_paths(g::AbstractGraph{T}) where T - 2 if is_cyclic(g) - 0 error("longest_paths(): input graph has cycles") - - end - 2 lps = Array[] - 2 max = 0 - 2 paths = all_paths(g) - 2 for path in paths # find length of longest path - 11 length(path) > max ? max = length(path) : nothing - - end - 2 for path in paths - 11 length(path) == max ? push!(lps, path) : nothing - - end - 2 return lps - - end - - - - # determine the number of edges crossing a graph cut, where s is the set of vertices on one side of the cut, - - # and the other side are the remanining vertices in g. - - """ - - edge_crossing(g, s) - - - - Given a graph ``g=(V,E)``,and a set of vertices ``s \\subseteq V``, determine the number of edges - - crossing the cut determined by the partition ``(s,V-s)``. - - - - # Arguments - - Required: - - - `g::AbstractGraph` : acylic graph. - - - `s::Array{Int}` : array of vertex indicies. - - - - ```julia-repl - - julia> cut_size = edge_crossing(g, s) - - ``` - - """ - - function edge_crossings(g::AbstractGraph{T}, s::Array{Int,1}) where T - - total = 0 - - d = convert(Array{Int,1}, vertices(g)) # collect the graph vertex ids in a integer array - - filter!(x->x ∉ s, d) # remove the vertex ids in s from d - - for v in s - - total += edge_crossings(g, v, d) - - end - - return total - - end - - - - # find number of crossing from a single vertex to all vertices in some vertex set d - - function edge_crossings(g::AbstractGraph{T}, s::Int, d::Array{Int,1}) where T - - total = 0 - - for v in d - - has_edge(g, s, v) ? total += 1 : nothing - - end - - return total - - end diff --git a/src/RequirementsAnalytics.jl.33665.cov b/src/RequirementsAnalytics.jl.33665.cov deleted file mode 100644 index 7e3f69e..0000000 --- a/src/RequirementsAnalytics.jl.33665.cov +++ /dev/null @@ -1,157 +0,0 @@ - - #File: DegreeRequirementsAnalytics.jl - - - - # Pre-order traverse a requirement tree, performing visit() on each requirement - 38 function preorder_traversal(root::AbstractRequirement, visit::Function = x -> nothing) - 8 if typeof(root) == CourseSet || length(root.requirements) == 0 - 1 return [root] - - end - 3 stack = Array{AbstractRequirement,1}([root]) - 3 visit_order = Array{AbstractRequirement,1}() - 33 while (length(stack) != 0) - 30 req = pop!(stack) - 30 visit(req) - 30 push!(visit_order, req) - 30 if typeof(req) == RequirementSet # course-set requirements have no children - 7 for r in reverse(req.requirements) - 27 push!(stack, r) - - end - - end - - end - 3 return visit_order - - end - - - - # Post-order traverse a requirement tree, performing visit() on each requirement - - function postorder_traversal(root::AbstractRequirement, visit::Function = x -> nothing) - - if typeof(root) == CourseSet || length(root.requirements) == 0 - - return [root] - - end - - stack = Array{AbstractRequirement,1}([root]) - - visit_order = Array{AbstractRequirement,1}() - - while (length(stack) != 0) - - req = pop!(stack) - - push!(visit_order, req) - - if typeof(req) == RequirementSet # course-set requirements have no children - - for r in req.requirements - - push!(stack, r) - - end - - end - - end - - for r in reverse(visit_order) - - visit(r) - - end - - return reverse(visit_order) - - end - - - - # Determine the level of requirement requisite in a requirement tree rooted at root. - - # Uses two queues to search the requirement tree level by level, the count_que keeps track of how many requirements are on a given level. - - # If keyword argument "requisite" is supplied, the level of that requisite is returned; otherwise, a dictionary of requisites levels - - # for the entire tree is returned. - 4 function level(root::AbstractRequirement; requisite = nothing) - 4 level_dict = Dict{Int, Int}() # dictionary (Requirement ID, level) - 2 req_que = Queue{AbstractRequirement}() - 2 enqueue!(req_que, root) - 2 counter = 1 - 2 count_que = Queue{Int}() - 2 level = 1 - 27 while (length(req_que) != 0) - 26 r = dequeue!(req_que) - 26 counter = counter - 1 - 26 if !isnothing(requisite) && r.id == requisite.id - 1 return level - - else - 25 level_dict[r.id] = level - 25 if typeof(r) == RequirementSet - 6 enqueue!(count_que, length(r.requirements)) - 6 for c in r.requirements - 24 enqueue!(req_que, c) - - end - - end - - end - 25 if counter == 0 # finished processing a level - 5 level = level + 1 - 11 while length(count_que) != 0 - 6 counter = counter + dequeue!(count_que) # total number of requirement sets in the current level - - end - - end - - end - 1 if !isnothing(requisite) # a requirement was supplied, but it's not in the tree, an error - 0 error("requirment $(requisite.name) is not in requirement tree $(root.name)") - - else - 1 return level_dict - - end - - end - - - - """ - - Formatted print of the requirements associated with a requirements tree. - - - - show_requirements(root; ) - - - - # Arguments - - Required: - - - `root::AbstractRequirement` : the root of the requirements tree. - - Keyword: - - - `tab::String` : the string to use for tabbing. - - default is three spaces. - - - `satisfied:Dict{Int,Bool}` : a dictionary with format degree requirement id => (satisfaction code, array of satisfying courses), with - - satisfaction code 0 indicating the degree requirement is not satisfied, 1 indicating it is, and 2 indicating the degree requirement is only - - partially satisfied. If this argument is supplied, a color-code annotation is also added beside each requirement, indicating the satisfaction - - status of each degree requirement. Note: the `satisfied` function can be used to create to create a `satisfied` dictionary. - - - `display_limit::Int` : limit on the number of courses displayed from a `CourseSet` requirement. - - - - # Examples: - - ```julia-repl - - julia> model = assign_courses(transcript, program_requirements, applied_credits) - - julia> x = model.obj_dict[:x] - - julia> is_satisfied = Dict{Int,Tuple{Int,Array{Int,1}}}() - - julia> satisfied(program_requirements, coalesce_transcript(transcript), flatten_requirements(program_requirements), value.(x), is_satisfied) - - julia> show_requirement(program_requirements, satisfied=is_satisfied) - - ```` - - """ - - function show_requirements(root::AbstractRequirement; io::IO = stdout, tab = " ", - - satisfied::Dict{Int,Tuple{Int,Array{Int,1}}} = Dict{Int,Tuple{Int,Array{Int,1}}}(), display_limit::Int = 500) - - for req in preorder_traversal(root) - - depth = level(root, requisite=req) - - tabs = tab^depth - - print(io, tabs * " ├-") - - printstyled(io, "$(req.name) "; bold = true) - - if haskey(satisfied, req.id) - - if satisfied[req.id][1] == 2 - - printstyled(io, "[satisfied] "; color = :green) - - elseif satisfied[req.id][1] == 0 - - printstyled(io, "[not satisfied] "; color = :red) - - elseif satisfied[req.id][1] == 1 - - printstyled(io, "[partially satisfied] "; color = :yellow) - - end - - end - - if req.description != "" - - print(io, "($(req.description), requires: $(req.credit_hours) credit hours)") - - else - - print(io, "($(req.credit_hours) credit hours)") - - end - - if typeof(req) == RequirementSet - - if req.satisfy < length(req.requirements) - - print(io, ", satisfy: $(req.satisfy) of $(length(req.requirements)) subrequirements\n",) - - else - - print(io, ", satisfy: all $(length(req.requirements)) subrequirements\n") - - end - - else # requirement is a CourseSet - - print(io, "\n") - - for (i, c) in enumerate(req.course_reqs) - - print(io, tabs * tab * " ├-") - - if i <= display_limit - - if (haskey(satisfied, req.id)) - - c[1].id ∈ satisfied[req.id][2] ? color = :green : color = :black - - else # satisfed not passed to the function - - color = :black - - end - - c[1].prefix != "" ? printstyled(io, "$(c[1].prefix) ", color = color) : nothing - - c[1].num != "" ? printstyled(io, "$(c[1].num) ", color = color) : nothing - - printstyled(io, "$(c[1].name) ($(c[1].credit_hours) credit hours), minimum grade: $(grade(c[2]))\n", color=color) - - else - - println(io, "Course list truncated to $(display_limit) courses...") - - break - - end - - end - - end - - end - - end diff --git a/src/Simulation/Enrollment.jl.33665.cov b/src/Simulation/Enrollment.jl.33665.cov deleted file mode 100644 index a149e9c..0000000 --- a/src/Simulation/Enrollment.jl.33665.cov +++ /dev/null @@ -1,155 +0,0 @@ - - module Enrollment - - using CurricularAnalytics: Student, Course, co, pre, strict_co, custom - - - 8 function enroll!(current_term, simulation, max_credits) - 8 student_progress = simulation.student_progress - - - 8 terms = simulation.degree_plan.terms - 8 courses = simulation.degree_plan.curriculum.courses - - - 8 for (termnum, term) in enumerate(terms) - - # Iterate through courses - 32 for course in term.courses - - # Clear the array of enrolled students for the course - 64 course.metadata["students"] = Student[] - - - 64 for student in simulation.enrolled_students - - - - # Get the coreqs of the the course - 640 strict_coreq_ids = [k for (k, v) in course.requisites if v == strict_co] - - - - # Enroll in strictCoreqs first - 640 for strict_coreq_id in strict_coreq_ids - 0 strict_coreq = get_course_by_id(courses, strict_coreq_id) - - - 0 if canEnroll(student, strict_coreq, courses, student_progress, max_credits, current_term) - - # Enroll the student in the course - 0 push!(strict_coreq.metadata["students"], student) - - - - # Increment the course's enrollment counters - 0 strict_coreq.metadata["enrolled"] += 1 - 0 strict_coreq.metadata["termenrollment"][current_term] += 1 - - - - # Increse the student's term credits - 0 student.termcredits += strict_coreq.credit_hours - - end - - end - - - - # Get the coreqs of the the course - 640 coreq_ids = [k for (k, v) in course.requisites if v == co] - - - - # Enroll in coreqs - 640 for coreqId in coreq_ids - 80 coreq = get_course_by_id(courses, coreqId) - - - 80 if canEnroll(student, coreq, courses, student_progress, max_credits, current_term) - - # Enroll the student in the course - 10 push!(coreq.metadata["students"], student) - - - - # Increment the course's enrollment counters - 10 coreq.metadata["enrolled"] += 1 - 10 coreq.metadata["termenrollment"][current_term] += 1 - - - - # Increse the student's term credits - 10 student.termcredits += coreq.credit_hours - - end - - end - - - - - - - - # Determine wheter the student can be enrolled in the current course. - 640 if canEnroll(student, course, courses, student_progress, max_credits, current_term) - - - - # Enroll the student in the course - 160 push!(course.metadata["students"], student) - - - - # Increment the course's enrollment counters - 160 course.metadata["enrolled"] += 1 - 160 course.metadata["termenrollment"][current_term] += 1 - - - - # Increse the student's term credits - 160 student.termcredits += course.credit_hours - - end - - end - - end - - end - - end - - - - # Function that determines wheter a student can enroll in a course - 720 function canEnroll(student, course, courses, student_progress, max_credits, term) - - # Find the prereq ids of the current course - 720 prereqs = get_reqs(courses, course, pre) - 1040 prereq_ids = map(x -> x.metadata["id"], prereqs) - - - - # Stuent is enrolled already - 720 if in(student, course.metadata["students"]) - 10 return false - - end - - - - # Student needs to complete prereqs - 710 if (length(prereq_ids) != 0 && sum(student_progress[student.id, prereq_ids]) != length(prereq_ids)) - 170 return false - - end - - - - # The student has completed the course - 1080 if student_progress[student.id, course.metadata["id"]] != 0.0 - 130 return false - - end - - - - # The student will exceed the maximum number of credit hours - 410 if student.termcredits + course.credit_hours > max_credits - 240 return false - - end - - - - # The student must wait until the term req has been met - 170 if course.metadata["term_req"] > term - 0 return false - - end - - - - # The student isn't enrolled in or hasn't completed coreqs - 170 if !enrolled_in_coreqs(student, course, courses, student_progress) - 0 return false - - end - - - 170 return true - - end - - - - # Determines whether a student is enrolled in or has completed coreqs for a given course - 170 function enrolled_in_coreqs(student, course, courses, student_progress) - 170 enrolled = true - - - 170 coreqs = get_reqs(courses, course, strict_co) - - - 340 for coreq in coreqs - 0 enrolled = enrolled && (in(student, course.metadata["students"]) || student_progress[student.id, coreq.metadata["id"]] == 1.0) - - end - - - 170 return enrolled - - end - - - - # Get reqs of a given course - 890 function get_reqs(courses, target_course, req) - 890 reqs = Course[] - 890 req_ids = [] - - - 890 req_ids = [k for (k, v) in target_course.requisites if v == req] - - - 1540 for req_id in req_ids - 320 course = get_course_by_id(courses, req_id) - 320 if course !== nothing - 560 push!(reqs, course) - - end - - end - 890 return reqs - - end - - - - # Find the course based on given id - 400 function get_course_by_id(courses, id) - 400 for course in courses - 2560 if course.id == id - 400 return course - - end - - end - 0 return nothing - - end - - end diff --git a/src/Simulation/PassRate.jl.33665.cov b/src/Simulation/PassRate.jl.33665.cov deleted file mode 100644 index 48726ee..0000000 --- a/src/Simulation/PassRate.jl.33665.cov +++ /dev/null @@ -1,147 +0,0 @@ - - # Predicts whether a student will pass a course using the course's passrate - - # as a probability. - - - - module PassRate - - # Train the model - - - 4 function train(degree_plan; stopout_rates::Array{Number}=Array{Number}([0.0838, 0.1334, 0.0465, 0.0631, 0.0368, 0.0189, 0.0165])) - 4 for course in degree_plan.curriculum.courses - 16 model = Dict() - 16 model[:passrate] = course.passrate - 18 course.metadata["model"]= model - - end - - - 2 degree_plan.metadata["stopout_model"][:rates] = stopout_rates * 100 - - end - - - - # Predict grade - 160 function predict_grade(course, student) - 160 roll = rand() - - - 160 if roll <= course.metadata["model"][:passrate] - 80 return 4.0 - - else - 80 return 0.0 - - end - - end - - - - # Predict stopout - - function predict_stopout(student, current_term, model) - - if current_term > 7 - - return false - - else - - roll = rand(1:100) - - return roll <= model[:rates][current_term] - - end - - end - - end - - - - # Sets all course model passrates to given value - 2 function set_passrates(courses, passrate) - 2 for course in courses - 16 course.passrate = passrate - - end - - end - - - - # Read from a CSV file that contains all courses taken by all students of a certain academic period - - # Compute and set the pass rates for given courses based on the student performance in the file - - # If a course is not found, set its pass rate to a preset value - - # If a course is presented as a requirement (a set of courses), use the average pass rate of the set - - # Note: Only support Tier I General Education for now - - function set_passrates_from_csv(courses, csv_path, pass_rate) - - university_course_table = CSV.File(csv_path, delim = ',', silencewarnings = true) |> DataFrame - - - - passing_grades = ["A", "B", "C", "D", "P"] - - all_grades = ["A", "B", "C", "D", "P", "W", "F", "E", "S", "WS"] - - - - for course in courses - - prefix = string(strip(course.prefix)) - - num = string(strip(course.num)) - - - - # Compute the number of student passed the course, and number of student took the course - - num_passes = nrow(filter(row -> passrate_filter(row, prefix, num, passing_grades), university_course_table)) - - num_students_taken = nrow(filter(row -> passrate_filter(row, prefix, num, all_grades), university_course_table)) - - - - # The course is not found in the course table - - if (num_students_taken == 0) - - # The course is in a Tier I General Education, then use the average pass rate of them - - if (prefix == "" && num == "" && occursin("Tier I", course.name)) - - course_passrate, num_passes, num_students_taken = get_gen_tier_I_passrate(university_course_table, ["150", "160"], passing_grades, all_grades, pass_rate) - - course.passrate = course_passrate - - else - - course.passrate = pass_rate # Hard code the rest of the course to a preset value for now - - end - - else - - course.passrate = num_passes / num_students_taken # Computer the course pass rate - - end - - course.metadata["num_students_taken"] = num_students_taken - - course.metadata["num_students_passes"] = num_passes - - end - - end - - - - # Compute the average pass rate of Tier I General Education of given numbers - - function get_gen_tier_I_passrate(university_course_table, nums, passing_grades, all_grades, pass_rate) - - # Compute the number of student passed the course, and number of student took the course - - num_passes = nrow(filter(row -> passrate_filter(row, nums, passing_grades), university_course_table)) - - num_students_taken = nrow(filter(row -> passrate_filter(row, nums, all_grades), university_course_table)) - - - - if (num_students_taken == 0) - - return pass_rate, 0, 0 - - else - - return num_passes / num_students_taken, num_passes, num_students_taken - - end - - end - - - - # A course prefix, course number based filter. - - # The function returns true if the course taken by the student has the given prefix, number, and is in the grade range - - function passrate_filter(row, prefix, num, grades) - - if row[:course_prefix] == prefix && row[:course_num] == num - - for grade in grades - - if row[:grade] == grade - - return true - - end - - end - - end - - return false - - end - - - - # A course number based filter. - - # The function returns true if the number of course taken by the student contains the given number, and is in the grade range - - function passrate_filter(row, nums, grades) - - if start_with_nums(row[:course_num], nums) - - for grade in grades - - if row[:grade] == grade - - return true - - end - - end - - end - - return false - - end - - - - # Help function - - function start_with_nums(str, nums) - - for num in nums - - if startswith(str, num) - - return true - - end - - end - - return false - - end - - - - # Set passrate for a given course - - function set_passrate_for_course(course::Course, passrate::Float64) - - course.passrate = passrate - - end - - - - # set passrate based on given course prefix and number - - function set_passrate_for_course(degree_plan::DegreePlan, course_prefix::AbstractString, course_num::AbstractString, passrate::Float64) - - courses = degree_plan.curriculum.courses - - course_found = false - - for course in courses - - if course.prefix == course_prefix && course.num == course_num - - course.passrate = passrate - - course_found = true - - end - - end - - return course_found - - end diff --git a/src/Simulation/Simulation.jl.33665.cov b/src/Simulation/Simulation.jl.33665.cov deleted file mode 100644 index 2ef2ee4..0000000 --- a/src/Simulation/Simulation.jl.33665.cov +++ /dev/null @@ -1,238 +0,0 @@ - - # File: Simulation.jl - - - - using DataFrames - - using Graphs - - - - include("PassRate.jl") - - include("Enrollment.jl") - - include("Report.jl") - - - - # Simulation Function - - """ - - simulate(degree_plan, course_attempt_limit, students; ) - - - - Perform a simulation on a degree plan with given students. - - - - # Arguments - - Required: - - - `degree_plan::DegreePlan` : the degree plan the simulation will be using. - - - `course_attempt_limit::Int` : the maximum number of attampts for each course in the degree plan. - - - `students::Array{Student}` : the cohort will be used through the simulation. - - - - Keywords: - - - `performance_model::PassRate` : the pass rate model of the simulation. - - - `enrollment_model::Enrollment` : the enrollment model of the simulation. - - - `max_credits::Int` : the maximum number of total credits a student can enroll each semester. - - - `duration::Int` : the maximum number of semester allowed. It's ignored if duration_lock is set to false. - - - `duration_lock::Boolean` : whether a student is allowed to stay on the degree plan forever. - - - `stopouts::Boolean` : whether a student will stop during the course of completing the degree plan. - - - - Returns the simulation result that students in the cohort taking courses from the degree plan. For a given student, - - in the simulation process, the student takes courses described in the degree plan at each semester. The simulation results - - the simulated graduation rates (overall and at each term) and stopout rates (overall and at each term). - - The performance of each student depends on the the pass rate model. The likelihood of a student dropping out - - from the degree plan is determined by the stopout model. The pass rates for courses in the degree plan construct - - the pass rate model. By default, if a pass rate for a course is not provided, the pass rate will be 50% (pure random). - - The probabilities of a student dropping out at different semesters are the stopout model. - - - - ```julia-repl - - julia> simulation = simulate(degree_plan, course_attempt_limit, students) - - ``` - - """ - 4 function simulate(degree_plan::DegreePlan, course_attempt_limit::Int, students::Array{Student}; performance_model=PassRate, stopout_rates=Nothing, enrollment_model=Enrollment, max_credits=18, duration=8, duration_lock=false, stopouts=false) - - - - # Create the simulation object - 4 simulation = Simulation(deepcopy(degree_plan)) - - - - # Train the model with stopout rates - 2 if stopout_rates != Nothing - 0 performance_model.train(simulation.degree_plan, stopout_rates) - - else - 2 performance_model.train(simulation.degree_plan) - - end - - - 2 simulation.course_attempt_limit = course_attempt_limit - - - - # Determine the number of students used in the simulation - 2 num_students = length(students) - 2 simulation.num_students = num_students - - - - # Populate the enrolled students array with all students - 2 simulation.enrolled_students = deepcopy(students) - - - - # Reset simulation object - 2 simulation.graduated_students = Student[] - 2 simulation.stopout_students = Student[] - 2 simulation.reach_attempts_students = Student[] - 2 simulation.grad_rate = 0.0 - 8 simulation.term_grad_rates = zeros(duration) - 2 simulation.stopout_rate = 0.0 - 8 simulation.term_stopout_rates = zeros(duration) - 8 simulation.reach_attempts_rates = zeros(duration) - - - 2 num_courses = simulation.degree_plan.curriculum.num_courses - - - - # Assign each student a unique id - 2 for (i, student) in enumerate(simulation.enrolled_students) - 20 student.id = i - 160 student.termpassed = zeros(num_courses) - - end - - - - # Initialize matrix to track student performance - - # Each row represents a student and each column is associated with a course. - - # A 1 signifies that the student passed the course while a 0 indicates incomplete. - 160 simulation.student_progress = zeros(num_students, num_courses) - - - - # Matrix to hold the number of attempts a student has made at passing a course - 160 simulation.student_attemps = ones(num_students, num_courses) - - - - # Record number of simulation terms - 2 simulation.duration = duration - - - - # Initialize courses - 2 for course in simulation.degree_plan.curriculum.courses - 64 course.metadata["termenrollment"] = zeros(duration) - 64 course.metadata["termpassed"] = zeros(duration) - 18 course.metadata["students"] = Student[] - - end - - - - # Convenience variables - 2 terms = simulation.degree_plan.terms - 2 student_progress = simulation.student_progress - 2 student_attemps = simulation.student_attemps - - - - # Begin simulation - 4 for current_term = 1:duration - - # Enroll students in courses - 8 enrollment_model.enroll!(current_term, simulation, max_credits) - - - - # Predict Performance - 8 for (termnum, term) in enumerate(terms) - 32 for course in term.courses - 64 for student in course.metadata["students"] - - # Make prediction - 160 predicted_grade = performance_model.predict_grade(course, student) - - - 160 course_name = construct_course_name(course.prefix, course.num, course.name) - 160 student.performance[course_name] = predicted_grade - - - - # Record grade for the course - 160 push!(course.metadata["grades"], predicted_grade) - - - - # Check to see if the grade is passing - 160 if predicted_grade > 1.67 - - # Mark that the student passed the course - 80 student_progress[student.id, course.metadata["id"]] = 1.0 - - - - # Log the term which the student passed the course - 80 course.metadata["termpassed"][current_term] += 1 - - - 80 student.termpassed[course.metadata["id"]] = current_term - - else - - # Record the failure - 80 course.metadata["failures"] += 1 - - - - # Check if the student have reached max attempts for a course - 80 attempts = student_attemps[student.id, course.metadata["id"]] - 80 if attempts == course_attempt_limit - 20 push!(simulation.reach_attempts_students, student) - - # Student has to stopout - 20 student.stopout = true - - end - - # Increment the attempts - 80 student_attemps[student.id, course.metadata["id"]] += 1 - - end - - - - # Increment the students credit hours and points - 160 student.total_credits += course.credit_hours - 160 student.total_points += predicted_grade * course.credit_hours - - end - - end - - end - - - - - - # Process term performance - 8 for student in simulation.enrolled_students - - # Compute the student's GPA - 80 student.gpa = student.total_points / student.total_credits - - - - # Reset the student's term credits to 0 - 88 student.termcredits = 0 - - end - - - - - - # Determine whether a student has graduated - 8 graduated_student_ids = [] - 8 for (i, student) in enumerate(simulation.enrolled_students) - 80 if sum(student_progress[student.id, :]) == num_courses - - # Add the student to the array of graduated students - 10 push!(simulation.graduated_students, student) - - - - # Record the semester of graduation - 10 student.gradsem = current_term - - - - # Record the index of the student - 10 push!(graduated_student_ids, i) - - - 10 simulation.time_to_degree += current_term - - end - - end - - # Remove graduated students from enrolled array - 8 deleteat!(simulation.enrolled_students, graduated_student_ids) - - - - # Compute graduation rate as of the current term - 8 simulation.term_grad_rates[current_term] = length(simulation.graduated_students) / num_students - - - - - - # Determine stopouts - 8 if stopouts - 0 stopout_student_ids = [] - 0 for (i, student) in enumerate(simulation.enrolled_students) - - # Predict stopout for students who haven't decided to stopout - 0 if student.stopout == false - 0 student.stopout = performance_model.predict_stopout(student, current_term, simulation.degree_plan.metadata["stopout_model"]) - - end - - - 0 if student.stopout - - # Add to array of stopouts - 0 push!(simulation.stopout_students, student) - - - - # Record index of the student - 0 push!(stopout_student_ids, i) - - end - - end - - - - # Remove stoped-out students from the array of enrolled students - 0 deleteat!(simulation.enrolled_students, stopout_student_ids) - - - - # Compute stopout rate as of the current term - 0 simulation.term_stopout_rates[current_term] = length(simulation.stopout_students) / num_students - - end - - - - # Compute reach course max attampts rate of the current term - 8 simulation.reach_attempts_rates[current_term] = round(length(simulation.reach_attempts_students) / num_students, digits=3) - - - - # Check to see if all students have graduated - 8 if length(simulation.enrolled_students) == 0 && !duration_lock - 1 simulation.duration = current_term - 8 break # breaks out of the simulation loop, i.e., stops the simulation - - end - - end - - - - # Compute graduation rate - 2 simulation.grad_rate = length(simulation.graduated_students) / num_students - - - - # Compute stopout rate - 2 simulation.stopout_rate = length(simulation.stopout_students) / num_students - - - - # Compute average time to degree - 2 simulation.time_to_degree /= length(simulation.graduated_students) - - - 2 return simulation - - end - - - - - - # Helper funcions - 160 function construct_course_name(prefix, num, name) - 160 return prefix * " " * string(num) * " " * name - - end diff --git a/test/DataHandler.jl.33665.cov b/test/DataHandler.jl.33665.cov deleted file mode 100644 index 8bebda3..0000000 --- a/test/DataHandler.jl.33665.cov +++ /dev/null @@ -1,305 +0,0 @@ - - # DataHandler tests - - - - @testset "DataHandler Tests" begin - - - - # test the data file format used for curricula - 2 curric = read_csv("./curriculum.csv") - - - 1 @test curric.name == "Underwater Basket Weaving" - 1 @test curric.institution == "ACME State University" - 1 @test curric.degree_type == "AA" - 1 @test curric.system_type == semester - 1 @test curric.CIP == "445786" - 1 @test length(curric.courses) == 12 - 1 @test curric.num_courses == 12 - 1 @test curric.credit_hours == 35 - - # test courses - 1 for (i,c) in enumerate(curric.courses) - 12 if c.id == 1 - 1 @test curric.courses[i].name == "Introduction to Baskets" - 1 @test curric.courses[i].credit_hours == 3 - 1 @test curric.courses[i].prefix == "BW" - 1 @test curric.courses[i].num == "110" - 1 @test curric.courses[i].institution == "ACME State University" - 1 @test curric.courses[i].canonical_name == "Baskets I" - 1 @test length(curric.courses[i].requisites) == 0 - 11 elseif c.id == 2 - 1 @test curric.courses[i].name == "Swimming" - 1 @test curric.courses[i].credit_hours == 3 - 1 @test curric.courses[i].prefix == "PE" - 1 @test curric.courses[i].num == "115" - 1 @test curric.courses[i].institution == "ACME State University" - 1 @test curric.courses[i].canonical_name == "Physical Education" - 1 @test length(curric.courses[i].requisites) == 0 - 10 elseif c.id == 3 - 1 @test curric.courses[i].name == "Introductory Calculus w/ Basketry Applications" - 1 @test curric.courses[i].credit_hours == 4 - 1 @test curric.courses[i].prefix == "MA" - 1 @test curric.courses[i].num == "116" - 1 @test curric.courses[i].institution == "ACME State University" - 1 @test curric.courses[i].canonical_name == "Calculus I" - 1 @test length(curric.courses[i].requisites) == 0 - 9 elseif c.id == 4 - 1 @test curric.courses[i].name == "Basic Basket Forms" - 1 @test curric.courses[i].credit_hours == 3 - 1 @test curric.courses[i].prefix == "BW" - 1 @test curric.courses[i].num == "111" - 1 @test curric.courses[i].institution == "ACME State University" - 1 @test curric.courses[i].canonical_name == "Baskets II" - 1 @test curric.courses[i].requisites[1] == pre - 1 @test curric.courses[i].requisites[5] == strict_co - 8 elseif c.id == 5 - 1 @test curric.courses[i].name == "Basic Basket Forms Lab" - 1 @test curric.courses[i].credit_hours == 1 - 1 @test curric.courses[i].prefix == "BW" - 1 @test curric.courses[i].num == "111L" - 1 @test curric.courses[i].institution == "ACME State University" - 1 @test curric.courses[i].canonical_name == "Baskets II Laboratory" - 1 @test length(curric.courses[i].requisites) == 0 - 7 elseif c.id == 6 - 1 @test curric.courses[i].name == "Advanced Basketry" - 1 @test curric.courses[i].credit_hours == 3 - 1 @test curric.courses[i].prefix == "BW" - 1 @test curric.courses[i].num == "201" - 1 @test curric.courses[i].institution == "ACME State University" - 1 @test curric.courses[i].canonical_name == "Baskets III" - 1 @test curric.courses[i].requisites[4] == pre - 1 @test curric.courses[i].requisites[5] == pre - 1 @test curric.courses[i].requisites[3] == co - 6 elseif c.id == 7 - 1 @test curric.courses[i].name == "Basket Materials & Decoration" - 1 @test curric.courses[i].credit_hours == 3 - 1 @test curric.courses[i].prefix == "BW" - 1 @test curric.courses[i].num == "214" - 1 @test curric.courses[i].institution == "ACME State University" - 1 @test curric.courses[i].canonical_name == "Basket Materials" - 1 @test curric.courses[i].requisites[1] == pre - 5 elseif c.id == 8 - 1 @test curric.courses[i].name == "Underwater Weaving" - 1 @test curric.courses[i].credit_hours == 3 - 1 @test curric.courses[i].prefix == "BW" - 1 @test curric.courses[i].num == "301" - 1 @test curric.courses[i].institution == "ACME State University" - 1 @test curric.courses[i].canonical_name == "Baskets IV" - 1 @test curric.courses[i].requisites[2] == pre - 1 @test curric.courses[i].requisites[7] == co - 4 elseif c.id == 9 - 1 @test curric.courses[i].name == "Humanitites Elective" - 1 @test curric.courses[i].credit_hours == 3 - 1 @test curric.courses[i].institution == "ACME State University" - 1 @test length(curric.courses[i].requisites) == 0 - 3 elseif c.id == 10 - 1 @test curric.courses[i].name == "Social Sciences Elective" - 1 @test curric.courses[i].credit_hours == 3 - 1 @test curric.courses[i].institution == "ACME State University" - 1 @test length(curric.courses[i].requisites) == 0 - 2 elseif c.id == 11 - 1 @test curric.courses[i].name == "Technical Elective" - 1 @test curric.courses[i].credit_hours == 3 - 1 @test curric.courses[i].institution == "ACME State University" - 1 @test length(curric.courses[i].requisites) == 0 - 1 elseif c.id == 12 - 1 @test curric.courses[i].name == "General Elective" - 1 @test curric.courses[i].credit_hours == 3 - 1 @test curric.courses[i].institution == "ACME State University" - 13 @test length(curric.courses[i].requisites) == 0 - - end - - end - - # TODO: add learning outcomes - - - - # test the data file format used for degree plans - 1 dp = read_csv("degree_plan.csv") - - - 1 @test dp.name == "4-Term Plan" - 1 @test dp.curriculum.name == "Underwater Basket Weaving" - 1 @test dp.curriculum.institution == "ACME State University" - 1 @test dp.curriculum.degree_type == "AA" - 1 @test dp.curriculum.system_type == semester - 1 @test dp.curriculum.CIP == "445786" - 1 @test length(dp.curriculum.courses)-length(dp.additional_courses) == 12 - 1 @test dp.num_terms == 4 - 1 @test dp.credit_hours == 45 - 1 @test length(dp.additional_courses) == 4 - - # test courses -- same tests as in the above curriculum, but a few additional courses - - # have been added, as well as a new requisite to an existing courses. - - # test courses - 1 curric = dp.curriculum - 1 for (i,c) in enumerate(curric.courses) - 16 if c.id == 1 - 1 @test curric.courses[i].name == "Introduction to Baskets" - 1 @test curric.courses[i].credit_hours == 3 - 1 @test curric.courses[i].prefix == "BW" - 1 @test curric.courses[i].num == "110" - 1 @test curric.courses[i].institution == "ACME State University" - 1 @test curric.courses[i].canonical_name == "Baskets I" - 1 @test length(curric.courses[i].requisites) == 0 - 15 elseif c.id == 2 - 1 @test curric.courses[i].name == "Swimming" - 1 @test curric.courses[i].credit_hours == 3 - 1 @test curric.courses[i].prefix == "PE" - 1 @test curric.courses[i].num == "115" - 1 @test curric.courses[i].institution == "ACME State University" - 1 @test curric.courses[i].canonical_name == "Physical Education" - 1 @test length(curric.courses[i].requisites) == 0 - 14 elseif c.id == 3 - 1 @test curric.courses[i].name == "Introductory Calculus w/ Basketry Applications" - 1 @test curric.courses[i].credit_hours == 4 - 1 @test curric.courses[i].prefix == "MA" - 1 @test curric.courses[i].num == "116" - 1 @test curric.courses[i].institution == "ACME State University" - 1 @test curric.courses[i].canonical_name == "Calculus I" - - # this is the only difference from above tests - 1 @test curric.courses[i].requisites[13] == pre - 13 elseif c.id == 4 - 1 @test curric.courses[i].name == "Basic Basket Forms" - 1 @test curric.courses[i].credit_hours == 3 - 1 @test curric.courses[i].prefix == "BW" - 1 @test curric.courses[i].num == "111" - 1 @test curric.courses[i].institution == "ACME State University" - 1 @test curric.courses[i].canonical_name == "Baskets II" - 1 @test curric.courses[i].requisites[1] == pre - 1 @test curric.courses[i].requisites[5] == strict_co - 12 elseif c.id == 5 - 1 @test curric.courses[i].name == "Basic Basket Forms Lab" - 1 @test curric.courses[i].credit_hours == 1 - 1 @test curric.courses[i].prefix == "BW" - 1 @test curric.courses[i].num == "111L" - 1 @test curric.courses[i].institution == "ACME State University" - 1 @test curric.courses[i].canonical_name == "Baskets II Laboratory" - 1 @test length(curric.courses[i].requisites) == 0 - 11 elseif c.id == 6 - 1 @test curric.courses[i].name == "Advanced Basketry" - 1 @test curric.courses[i].credit_hours == 3 - 1 @test curric.courses[i].prefix == "BW" - 1 @test curric.courses[i].num == "201" - 1 @test curric.courses[i].institution == "ACME State University" - 1 @test curric.courses[i].canonical_name == "Baskets III" - 1 @test curric.courses[i].requisites[4] == pre - 1 @test curric.courses[i].requisites[5] == pre - 1 @test curric.courses[i].requisites[3] == co - 10 elseif c.id == 7 - 1 @test curric.courses[i].name == "Basket Materials & Decoration" - 1 @test curric.courses[i].credit_hours == 3 - 1 @test curric.courses[i].prefix == "BW" - 1 @test curric.courses[i].num == "214" - 1 @test curric.courses[i].institution == "ACME State University" - 1 @test curric.courses[i].canonical_name == "Basket Materials" - 1 @test curric.courses[i].requisites[1] == pre - 9 elseif c.id == 8 - 1 @test curric.courses[i].name == "Underwater Weaving" - 1 @test curric.courses[i].credit_hours == 3 - 1 @test curric.courses[i].prefix == "BW" - 1 @test curric.courses[i].num == "301" - 1 @test curric.courses[i].institution == "ACME State University" - 1 @test curric.courses[i].canonical_name == "Baskets IV" - 1 @test curric.courses[i].requisites[2] == pre - 1 @test curric.courses[i].requisites[7] == co - 8 elseif c.id == 9 - 1 @test curric.courses[i].name == "Humanitites Elective" - 1 @test curric.courses[i].credit_hours == 3 - 1 @test curric.courses[i].institution == "ACME State University" - 1 @test length(curric.courses[i].requisites) == 0 - 7 elseif c.id == 10 - 1 @test curric.courses[i].name == "Social Sciences Elective" - 1 @test curric.courses[i].credit_hours == 3 - 1 @test curric.courses[i].institution == "ACME State University" - 1 @test length(curric.courses[i].requisites) == 0 - 6 elseif c.id == 11 - 1 @test curric.courses[i].name == "Technical Elective" - 1 @test curric.courses[i].credit_hours == 3 - 1 @test curric.courses[i].institution == "ACME State University" - 1 @test length(curric.courses[i].requisites) == 0 - 5 elseif c.id == 12 - 1 @test curric.courses[i].name == "General Elective" - 1 @test curric.courses[i].credit_hours == 3 - 1 @test curric.courses[i].institution == "ACME State University" - 17 @test length(curric.courses[i].requisites) == 0 - - end - - end - - # test additional courses - 1 for (i,c) in enumerate(dp.additional_courses) - 4 if c.id == 13 - 1 @test dp.additional_courses[i].name == "Precalculus w/ Basketry Applications" - 1 @test dp.additional_courses[i].credit_hours == 3 - 1 @test dp.additional_courses[i].prefix == "MA" - 1 @test dp.additional_courses[i].num == "110" - 1 @test dp.additional_courses[i].institution == "ACME State University" - 1 @test dp.additional_courses[i].canonical_name == "Precalculus" - 1 @test dp.additional_courses[i].requisites[14] == pre - 3 elseif c.id == 14 - 1 @test dp.additional_courses[i].name == "College Algebra" - 1 @test dp.additional_courses[i].credit_hours == 3 - 1 @test dp.additional_courses[i].prefix == "MA" - 1 @test dp.additional_courses[i].num == "102" - 1 @test dp.additional_courses[i].institution == "ACME State University" - 1 @test dp.additional_courses[i].canonical_name == "College Algebra" - 1 @test dp.additional_courses[i].requisites[15] == strict_co - 2 elseif c.id == 15 - 1 @test dp.additional_courses[i].name == "College Algebra Studio" - 1 @test dp.additional_courses[i].credit_hours == 1 - 1 @test dp.additional_courses[i].prefix == "MA" - 1 @test dp.additional_courses[i].num == "102S" - 1 @test dp.additional_courses[i].institution == "ACME State University" - 1 @test dp.additional_courses[i].canonical_name == "College Algebra Recitation" - 1 @test length(dp.additional_courses[i].requisites) == 0 - 1 elseif c.id == 16 - 1 @test dp.additional_courses[i].name == "Hemp Baskets" - 1 @test dp.additional_courses[i].credit_hours == 3 - 1 @test dp.additional_courses[i].prefix == "BW" - 1 @test dp.additional_courses[i].num == "420" - 1 @test dp.additional_courses[i].institution == "ACME State University" - 1 @test dp.additional_courses[i].canonical_name == "College Algebra Recitation" - 5 @test dp.additional_courses[i].requisites[6] == co - - end - - end - - # TODO: add learning outcomes - - - - # Create a curriculum and degree plan, and test read/write invariance for both - - # 8-vertex test curriculum - valid - - # - - # A --------* C --------* E - - # */ |* - - # / | - - # B-------/ D --------* F - - # - - # (A,C) - pre; (C,E) - pre; (B,C) - pre; (D,C) - co; (C,E) - pre; (D,F) - pre - - # - - - 1 A = Course("Introduction to Baskets", 3, institution="ACME State University", prefix="BW", num="101", canonical_name="Baskets I") - 1 B = Course("Swimming", 3, institution="ACME State University", prefix="PE", num="115", canonical_name="Physical Education") - 1 C = Course("Basic Basket Forms", 3, institution="ACME State University", prefix="BW", num="111", canonical_name="Baskets I") - 1 D = Course("Basic Basket Forms Lab", 1, institution="ACME State University", prefix="BW", num="111L", canonical_name="Baskets I Laboratory") - 1 E = Course("Advanced Basketry", 3, institution="ACME State University", prefix="CS", num="300", canonical_name="Baskets II") - 1 F = Course("Basket Materials & Decoration", 3, institution="ACME State University", prefix="BW", num="214", canonical_name="Basket Materials") - - - 1 add_requisite!(A,C,pre) - 1 add_requisite!(B,C,pre) - 1 add_requisite!(D,C,co) - 1 add_requisite!(C,E,pre) - 1 add_requisite!(D,F,pre) - - - 6 curric1 = Curriculum("Underwater Basket Weaving", [A,B,C,D,E,F], institution="ACME State University", CIP="445786") - - # write curriculum to secondary storage - 1 @test write_csv(curric1, "./UBW-curric.csv") == true - - # read from same location - 1 curric2 = read_csv("./UBW-curric.csv") - 1 @test string(curric1) == string(curric2) # read/write invariance test - 1 rm("./UBW-curric.csv") - - - 1 terms = Array{Term}(undef, 3) - 2 terms[1] = Term([A,B]) - 2 terms[2] = Term([C,D]) - 2 terms[3] = Term([E,F]) - - - 1 dp1 = DegreePlan("3-term UBW plan", curric1, terms) - - # write degree plan to secondary storage - 1 @test write_csv(dp1, "UBW-degree-plan.csv") == true - - # read from same location - 1 dp2 = read_csv("./UBW-degree-plan.csv") - - - 1 @test string(dp1) == string(dp2) # read/write invariance test - - - 1 rm("./UBW-degree-plan.csv") - - - - - - end diff --git a/test/DegreePlanCreation.jl.33665.cov b/test/DegreePlanCreation.jl.33665.cov deleted file mode 100644 index 57650d4..0000000 --- a/test/DegreePlanCreation.jl.33665.cov +++ /dev/null @@ -1,75 +0,0 @@ - - # DegreePlanCreation tests - - - - @testset "DegreePlanCreation Tests" begin - - # - - # 4-course curriculum - only one possible degree plan w/ max_cpt - - # - - # A --------* B - - # - - # - - # C --------* D - - # - - # (A,B) - pre; (C,D) - pre - - - 2 A = Course("A", 3, institution="ACME State", prefix="BW", num="101", canonical_name="Baskets I") - 1 B = Course("B", 3, institution="ACME State", prefix="BW", num="201", canonical_name="Baskets II") - 1 C = Course("C", 3, institution="ACME State", prefix="BW", num="102", canonical_name="Basket Apps I") - 1 D = Course("D", 3, institution="ACME State", prefix="BW", num="202", canonical_name="Basket Apps II") - - - 1 add_requisite!(A,B,pre) - 1 add_requisite!(C,D,pre) - - - 4 curric = Curriculum("Basket Weaving", [A,B,C,D], institution="ACME State") - - - 1 terms = bin_filling(curric, max_cpt=6) - - - 1 @test terms[1].courses[1].name == "A" || terms[1].courses[1].name == "C" - 2 @test terms[1].courses[2].name == "A" || terms[1].courses[2].name == "C" - 1 @test terms[2].courses[1].name == "B" || terms[2].courses[1].name == "D" - 2 @test terms[2].courses[2].name == "B" || terms[2].courses[2].name == "D" - - - - # - - # 4-course curriculum - only one possible degree plan w/ max_cpt - - # - - # A C - - # | | - - # | | - - # * * - - # B D - - # - - # (A,B) - strict_co; (C,D) - strict_co - - - 1 A = Course("A", 3, institution="ACME State", prefix="BW", num="101", canonical_name="Baskets I") - 1 B = Course("B", 3, institution="ACME State", prefix="BW", num="201", canonical_name="Baskets II") - 1 C = Course("C", 3, institution="ACME State", prefix="BW", num="102", canonical_name="Basket Apps I") - 1 D = Course("D", 3, institution="ACME State", prefix="BW", num="202", canonical_name="Basket Apps II") - - - 1 add_requisite!(A,B,strict_co) - 1 add_requisite!(C,D,strict_co) - - - 4 curric = Curriculum("Basket Weaving", [A,B,C,D], institution="ACME State") - - - 1 terms = bin_filling(curric, max_cpt=6) - - - 1 @test terms[1].courses[1].name == "A" || terms[1].courses[1].name == "B" - 2 @test terms[1].courses[2].name == "A" || terms[1].courses[2].name == "B" - 1 @test terms[2].courses[1].name == "C" || terms[2].courses[1].name == "D" - 2 @test terms[2].courses[2].name == "C" || terms[2].courses[2].name == "D" - - - 1 dp = create_degree_plan(curric, max_cpt=6) - 1 @test nv(dp.curriculum.graph) == 4 - 1 @test ne(dp.curriculum.graph) == 2 - 1 for term in dp.terms - 2 credits = 0 - 2 for c in term.courses - 4 credits += c.credit_hours - - end - 2 @test credits >= 3 - 2 @test credits <= 6 - - end - 1 @test dp.terms[1].courses[1].name == "A" || dp.terms[1].courses[1].name == "B" - 2 @test dp.terms[1].courses[2].name == "A" || dp.terms[1].courses[2].name == "B" - 1 @test dp.terms[2].courses[1].name == "C" || dp.terms[2].courses[1].name == "D" - 2 @test dp.terms[2].courses[2].name == "C" || dp.terms[2].courses[2].name == "D" - - - - end