From a4e391b6f0a2294277f16c0520e29c5052be0a7a Mon Sep 17 00:00:00 2001 From: Greg Heileman Date: Fri, 15 Sep 2023 18:08:38 -0700 Subject: [PATCH 01/13] New requisite data structure (single clause) --- UBW-curric.csv | 13 +++++++ src/CSVUtilities.jl | 2 +- src/CurricularAnalytics.jl | 8 ++-- src/DataTypes/Course.jl | 25 ++++++++----- src/DataTypes/Curriculum.jl | 41 ++++++++++++-------- src/DataTypes/DegreePlan.jl | 6 +-- src/DegreePlanAnalytics.jl | 2 +- src/DegreePlanCreation.jl | 4 +- src/Simulation/Enrollment.jl | 6 +-- test/DataHandler.jl | 72 ++++++++++++++++++------------------ test/DataTypes.jl | 14 +++---- 11 files changed, 111 insertions(+), 82 deletions(-) create mode 100644 UBW-curric.csv diff --git a/UBW-curric.csv b/UBW-curric.csv new file mode 100644 index 00000000..5c6d2d79 --- /dev/null +++ b/UBW-curric.csv @@ -0,0 +1,13 @@ +Curriculum,Underwater Basket Weaving,,,,,,,,, +Institution,"ACME State University",,,,,,,,, +Degree Type,"BS",,,,,,,,, +System Type,"semester",,,,,,,,, +CIP,"445786",,,,,,,,, +Courses,,,,,,,,,, +Course ID,Course Name,Prefix,Number,Prerequisites,Corequisites,Strict-Corequisites,Credit Hours,Institution,Canonical Name +300052638,"Basket Materials & Decoration","BW","214","4037212392",,,3,"ACME State University","Basket Materials", +1050322132,"Introduction to Baskets","BW","101",,,,3,"ACME State University","Baskets I", +1888571219,"Swimming","PE","115",,,,3,"ACME State University","Physical Education", +2012165139,"Advanced Basketry","CS","300","3989163861",,,3,"ACME State University","Baskets II", +3989163861,"Basic Basket Forms","BW","111","1050322132;1888571219","4037212392",,3,"ACME State University","Baskets I", +4037212392,"Basic Basket Forms Lab","BW","111L",,,,1,"ACME State University","Baskets I Laboratory", \ No newline at end of file diff --git a/src/CSVUtilities.jl b/src/CSVUtilities.jl index f78f2045..82900746 100644 --- a/src/CSVUtilities.jl +++ b/src/CSVUtilities.jl @@ -47,7 +47,7 @@ function course_line(course, term_id; metrics=false) course_prereq = "\"" course_coreq = "\"" course_scoreq = "\"" - for requesite in course.requisites + for requesite in course.requisites[1] if requesite[2] == pre course_prereq = course_prereq * string(requesite[1]) * ";" elseif requesite[2] == co diff --git a/src/CurricularAnalytics.jl b/src/CurricularAnalytics.jl index be581c6e..cfd03ee3 100644 --- a/src/CurricularAnalytics.jl +++ b/src/CurricularAnalytics.jl @@ -77,7 +77,7 @@ function extraneous_requisites(c::Curriculum; print=false) remove = true for n in nb # check for co- or strict_co requisites if has_path(c.graph, n, v) # is there a path from n to v? - req_type = c.courses[n].requisites[c.courses[u].id] # the requisite relationship between u and n + req_type = c.courses[n].requisites[1][c.courses[u].id] # the requisite relationship between u and n if (req_type == co) || (req_type == strict_co) # is u a co or strict_co requisite for n? remove = false # a co or strict_co relationshipo is involved, must keep (u, v) end @@ -654,19 +654,19 @@ function merge_curricula(name::AbstractString, c1::Curriculum, c2::Curriculum, m for (j,c) in enumerate(extra_courses) # print("\n $(c.name): ") # print("total requisistes = $(length(c.requisites)),") - for req in keys(c.requisites) + for req in keys(c.requisites[1]) # 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]) + add_requisite!(req_course, new_courses[j], c.requisites[1][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]) + add_requisite!(new_courses[i], new_courses[j], c.requisites[1][req]) else # requisite is neither in c1 or 2 -- this shouldn't happen => error error("requisite error on course: $(c.name)") end diff --git a/src/DataTypes/Course.jl b/src/DataTypes/Course.jl index 5a347384..7a09444b 100644 --- a/src/DataTypes/Course.jl +++ b/src/DataTypes/Course.jl @@ -56,7 +56,7 @@ mutable struct Course <: AbstractCourse 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 + requisites::Array{Dict{Int, Requisite},1} # Array of requisite clauses, each clause 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 @@ -82,14 +82,13 @@ mutable struct Course <: AbstractCourse this.department = department this.cross_listed = cross_listed this.canonical_name = canonical_name - this.requisites = Dict{Int, Requisite}() - #this.requisite_formula + this.requisites = Array{Dict{Int, Requisite},1}() + push!(this.requisites, Dict{Int, Requisite}()) # create an empty first clause for requisites this.metrics = Dict{String, Any}() this.metadata = Dict{String, Any}() this.learning_outcomes = learning_outcomes this.vertex_id = Dict{Int, Int}() # curriculum id -> vertex id, note: course may be in multiple curricula - this.passrate = passrate return this end @@ -106,7 +105,7 @@ mutable struct CourseCollection <: AbstractCourse 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 + requisites::Array{Dict{Int, Requisite},1} # Array of requisite clauses, each clause in (requisite_course id, requisite_type) format metrics::Dict{String, Any} # Course-related metrics metadata::Dict{String, Any} # Course-related metadata @@ -126,8 +125,8 @@ mutable struct CourseCollection <: AbstractCourse this.college = college this.department = department this.canonical_name = canonical_name - this.requisites = Dict{Int, Requisite}() - #this.requisite_formula + this.requisites = Array{Dict{Int, Requisite},1}() + push!(this.requisites, Dict{Int, Requisite}()) # create an empty first clause for requisites this.metrics = Dict{String, Any}() this.metadata = Dict{String, Any}() this.vertex_id = Dict{Int, Int}() # curriculum id -> vertex id @@ -157,7 +156,7 @@ One of the following requisite types must be specified for the `requisite_type`: - `strict_co` : a strict co-requisite course that must be taken at the same time as `tc`. """ function add_requisite!(requisite_course::AbstractCourse, course::AbstractCourse, requisite_type::Requisite) - course.requisites[requisite_course.id] = requisite_type + course.requisites[1][requisite_course.id] = requisite_type end """ @@ -180,10 +179,16 @@ The following requisite types may be specified for the `requisite_type`: function add_requisite!(requisite_courses::Array{AbstractCourse}, course::AbstractCourse, requisite_types::Array{Requisite}) @assert length(requisite_courses) == length(requisite_types) for i = 1:length(requisite_courses) - course.requisites[requisite_courses[i].id] = requisite_types[i] + course.requisites[1][requisite_courses[i].id] = requisite_types[i] end end + +function add_requisite!(requisite_course::AbstractCourse, course::AbstractCourse, clause::Int, requisite_type::Requisite) + +end + + """ delete_requisite!(rc, tc) @@ -200,5 +205,5 @@ 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 - delete!(course.requisites, requisite_course.id) + delete!(course.requisites[1], requisite_course.id) end diff --git a/src/DataTypes/Curriculum.jl b/src/DataTypes/Curriculum.jl index 395c7a7b..9c2182f6 100644 --- a/src/DataTypes/Curriculum.jl +++ b/src/DataTypes/Curriculum.jl @@ -46,7 +46,10 @@ mutable struct Curriculum metadata::Dict{String, Any} # Curriculum-related metadata # Constructor - function Curriculum(name::AbstractString, courses::Array{AbstractCourse}; learning_outcomes::Array{LearningOutcome}=Array{LearningOutcome,1}(), + #function Curriculum(name::AbstractString, courses::Array{AbstractCourse}; requisite_clauses::Dict{Course, Int}=Dict{Course, Int}(), 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) + function Curriculum(name::AbstractString, courses::Array{AbstractCourse}; requisite_clauses::Dict{Course, Int}=Dict{Course, Int}(), 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) this = new() @@ -71,6 +74,14 @@ mutable struct Curriculum create_graph!(this) this.metrics = Dict{String, Any}() this.metadata = Dict{String, Any}() + #= + requisite_clauses = requisite_clauses + if length(requisite_clauses == 0) + for c in this.courses + requisite_clauses[c] = 1 + end + end + =# this.learning_outcomes = learning_outcomes this.learning_outcome_graph = SimpleDiGraph{Int}() create_learning_outcome_graph!(this) @@ -84,11 +95,11 @@ mutable struct Curriculum return this end - 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) - 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) + function Curriculum(name::AbstractString, courses::Array{Course}; requisite_clauses::Dict{Course, Int}=Dict{Course, Int}(), + 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) + Curriculum(name, convert(Array{AbstractCourse},courses), requisite_clauses=requisite_clauses, learning_outcomes=learning_outcomes, + degree_type=degree_type, system_type=system_type, institution=institution, CIP=CIP, id=id, sortby_ID=sortby_ID) end end @@ -124,7 +135,7 @@ function is_valid(c::Curriculum, error_msg::IOBuffer=IOBuffer()) # 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. for course in c.courses - for (k,r) in course.requisites + for (k,r) in course.requisites[1] if r == strict_co v_d = course_from_id(c,course.id).vertex_id[c.id] # destination vertex v_s = course_from_id(c,k).vertex_id[c.id] # source vertex @@ -177,9 +188,9 @@ function convert_ids(curriculum::Curriculum) c1.id = mod(hash(c1.name * c1.prefix * c1.num * c1.institution), UInt32) if old_id != c1.id for c2 in curriculum.courses - if old_id in keys(c2.requisites) - add_requisite!(c1, c2, c2.requisites[old_id]) - delete!(c2.requisites, old_id) + if old_id in keys(c2.requisites[1]) + add_requisite!(c1, c2, c2.requisites[1][old_id]) + delete!(c2.requisites[1], old_id) end end end @@ -265,7 +276,7 @@ function create_graph!(curriculum::Curriculum) end mapped_vertex_ids = map_vertex_ids(curriculum) for c in curriculum.courses - for r in collect(keys(c.requisites)) + for r in collect(keys(c.requisites[1])) if add_edge!(curriculum.graph, mapped_vertex_ids[r], c.vertex_id[curriculum.id]) else s = course_from_id(curriculum, r) @@ -314,9 +325,9 @@ function create_course_learning_outcome_graph!(curriculum::Curriculum) # Add edges among courses for c in curriculum.courses - for r in collect(keys(c.requisites)) + for r in collect(keys(c.requisites[1])) if add_edge!(curriculum.course_learning_outcome_graph, mapped_vertex_ids[r], c.vertex_id[curriculum.id]) - set_prop!(curriculum.course_learning_outcome_graph, Edge(mapped_vertex_ids[r], c.vertex_id[curriculum.id]), :c_to_c, c.requisites[r]) + set_prop!(curriculum.course_learning_outcome_graph, Edge(mapped_vertex_ids[r], c.vertex_id[curriculum.id]), :c_to_c, c.requisites[1][r]) else s = course_from_id(curriculum, r) @@ -388,9 +399,9 @@ function requisite_type(curriculum::Curriculum, src_course_id::Int, dst_course_i dst = c end end - if ((src == 0 || dst == 0) || !haskey(dst.requisites, src.id)) + if ((src == 0 || dst == 0) || !haskey(dst.requisites[1], src.id)) error("edge ($src_course_id, $dst_course_id) does not exist in curriculum graph") else - return dst.requisites[src.id] + return dst.requisites[1][src.id] end end \ No newline at end of file diff --git a/src/DataTypes/DegreePlan.jl b/src/DataTypes/DegreePlan.jl index 9730c868..6f2c4265 100644 --- a/src/DataTypes/DegreePlan.jl +++ b/src/DataTypes/DegreePlan.jl @@ -127,7 +127,7 @@ function is_valid(plan::DegreePlan, error_msg::IOBuffer=IOBuffer()) for c in plan.terms[i].courses for j in i-1:-1:1 for k in plan.terms[j].courses - for l in keys(k.requisites) + for l in keys(k.requisites[1]) if l == c.id validity = false write(error_msg, "\n-Invalid requisite: $(c.name) in term $i is a requisite for $(k.name) in term $j") @@ -143,8 +143,8 @@ function is_valid(plan::DegreePlan, error_msg::IOBuffer=IOBuffer()) for r in plan.terms[i].courses if c == r continue - elseif haskey(c.requisites, r.id) - if c.requisites[r.id] == pre + elseif haskey(c.requisites[1], r.id) + if c.requisites[1][r.id] == pre validity = false write(error_msg, "\n-Invalid prerequisite: $(r.name) in term $i is a prerequisite for $(c.name) in the same term") end diff --git a/src/DegreePlanAnalytics.jl b/src/DegreePlanAnalytics.jl index 430dc8e2..8c2cfa41 100644 --- a/src/DegreePlanAnalytics.jl +++ b/src/DegreePlanAnalytics.jl @@ -102,7 +102,7 @@ The requisite distance metric computed by this function is stored in the associa function requisite_distance(plan::DegreePlan, course::Course) distance = 0 term = find_term(plan, course) - for req in keys(course.requisites) + for req in keys(course.requisites[1]) distance = distance + (term - find_term(plan, course_from_id(plan.curriculum, req))) end return course.metrics["requisite distance"] = distance diff --git a/src/DegreePlanCreation.jl b/src/DegreePlanCreation.jl index 94dbc7ec..2b33e853 100644 --- a/src/DegreePlanCreation.jl +++ b/src/DegreePlanCreation.jl @@ -29,7 +29,7 @@ function bin_filling(curric::Curriculum, additional_courses::Array{AbstractCours end # if c serves as a strict-corequisite for other courses, include them in current term too for course in UC - for req in course.requisites + for req in course.requisites[1] if req[1] == c.id if req[2] == strict_co deleteat!(UC, findfirst(isequal(course), UC)) @@ -66,7 +66,7 @@ function select_vertex(curric::Curriculum, term_courses::Array{AbstractCourse,1} if invariant1 == true invariant2 = true for c in term_courses - if c.id in collect(keys(target.requisites)) && target.requisites[c.id] == pre # AND shortcircuits, otherwise 2nd expression would error + if c.id in collect(keys(target.requisites[1])) && target.requisites[1][c.id] == pre # AND shortcircuits, otherwise 2nd expression would error invariant2 = false break # try a new target end diff --git a/src/Simulation/Enrollment.jl b/src/Simulation/Enrollment.jl index d4074b34..5151804d 100644 --- a/src/Simulation/Enrollment.jl +++ b/src/Simulation/Enrollment.jl @@ -16,7 +16,7 @@ module Enrollment for student in simulation.enrolled_students # Get the coreqs of the the course - strict_coreq_ids = [k for (k, v) in course.requisites if v == strict_co] + strict_coreq_ids = [k for (k, v) in course.requisites[1] if v == strict_co] # Enroll in strictCoreqs first for strict_coreq_id in strict_coreq_ids @@ -36,7 +36,7 @@ module Enrollment end # Get the coreqs of the the course - coreq_ids = [k for (k, v) in course.requisites if v == co] + coreq_ids = [k for (k, v) in course.requisites[1] if v == co] # Enroll in coreqs for coreqId in coreq_ids @@ -132,7 +132,7 @@ module Enrollment reqs = Course[] req_ids = [] - req_ids = [k for (k, v) in target_course.requisites if v == req] + req_ids = [k for (k, v) in target_course.requisites[1] if v == req] for req_id in req_ids course = get_course_by_id(courses, req_id) diff --git a/test/DataHandler.jl b/test/DataHandler.jl index ff2fc078..7288eeaa 100644 --- a/test/DataHandler.jl +++ b/test/DataHandler.jl @@ -22,7 +22,7 @@ for (i,c) in enumerate(curric.courses) @test curric.courses[i].num == "110" @test curric.courses[i].institution == "ACME State University" @test curric.courses[i].canonical_name == "Baskets I" - @test length(curric.courses[i].requisites) == 0 + @test length(curric.courses[i].requisites[1]) == 0 elseif c.id == 2 @test curric.courses[i].name == "Swimming" @test curric.courses[i].credit_hours == 3 @@ -30,7 +30,7 @@ for (i,c) in enumerate(curric.courses) @test curric.courses[i].num == "115" @test curric.courses[i].institution == "ACME State University" @test curric.courses[i].canonical_name == "Physical Education" - @test length(curric.courses[i].requisites) == 0 + @test length(curric.courses[i].requisites[1]) == 0 elseif c.id == 3 @test curric.courses[i].name == "Introductory Calculus w/ Basketry Applications" @test curric.courses[i].credit_hours == 4 @@ -38,7 +38,7 @@ for (i,c) in enumerate(curric.courses) @test curric.courses[i].num == "116" @test curric.courses[i].institution == "ACME State University" @test curric.courses[i].canonical_name == "Calculus I" - @test length(curric.courses[i].requisites) == 0 + @test length(curric.courses[i].requisites[1]) == 0 elseif c.id == 4 @test curric.courses[i].name == "Basic Basket Forms" @test curric.courses[i].credit_hours == 3 @@ -46,8 +46,8 @@ for (i,c) in enumerate(curric.courses) @test curric.courses[i].num == "111" @test curric.courses[i].institution == "ACME State University" @test curric.courses[i].canonical_name == "Baskets II" - @test curric.courses[i].requisites[1] == pre - @test curric.courses[i].requisites[5] == strict_co + @test curric.courses[i].requisites[1][1] == pre + @test curric.courses[i].requisites[1][5] == strict_co elseif c.id == 5 @test curric.courses[i].name == "Basic Basket Forms Lab" @test curric.courses[i].credit_hours == 1 @@ -55,7 +55,7 @@ for (i,c) in enumerate(curric.courses) @test curric.courses[i].num == "111L" @test curric.courses[i].institution == "ACME State University" @test curric.courses[i].canonical_name == "Baskets II Laboratory" - @test length(curric.courses[i].requisites) == 0 + @test length(curric.courses[i].requisites[1]) == 0 elseif c.id == 6 @test curric.courses[i].name == "Advanced Basketry" @test curric.courses[i].credit_hours == 3 @@ -63,9 +63,9 @@ for (i,c) in enumerate(curric.courses) @test curric.courses[i].num == "201" @test curric.courses[i].institution == "ACME State University" @test curric.courses[i].canonical_name == "Baskets III" - @test curric.courses[i].requisites[4] == pre - @test curric.courses[i].requisites[5] == pre - @test curric.courses[i].requisites[3] == co + @test curric.courses[i].requisites[1][4] == pre + @test curric.courses[i].requisites[1][5] == pre + @test curric.courses[i].requisites[1][3] == co elseif c.id == 7 @test curric.courses[i].name == "Basket Materials & Decoration" @test curric.courses[i].credit_hours == 3 @@ -73,7 +73,7 @@ for (i,c) in enumerate(curric.courses) @test curric.courses[i].num == "214" @test curric.courses[i].institution == "ACME State University" @test curric.courses[i].canonical_name == "Basket Materials" - @test curric.courses[i].requisites[1] == pre + @test curric.courses[i].requisites[1][1] == pre elseif c.id == 8 @test curric.courses[i].name == "Underwater Weaving" @test curric.courses[i].credit_hours == 3 @@ -81,28 +81,28 @@ for (i,c) in enumerate(curric.courses) @test curric.courses[i].num == "301" @test curric.courses[i].institution == "ACME State University" @test curric.courses[i].canonical_name == "Baskets IV" - @test curric.courses[i].requisites[2] == pre - @test curric.courses[i].requisites[7] == co + @test curric.courses[i].requisites[1][2] == pre + @test curric.courses[i].requisites[1][7] == co elseif c.id == 9 @test curric.courses[i].name == "Humanitites Elective" @test curric.courses[i].credit_hours == 3 @test curric.courses[i].institution == "ACME State University" - @test length(curric.courses[i].requisites) == 0 + @test length(curric.courses[i].requisites[1]) == 0 elseif c.id == 10 @test curric.courses[i].name == "Social Sciences Elective" @test curric.courses[i].credit_hours == 3 @test curric.courses[i].institution == "ACME State University" - @test length(curric.courses[i].requisites) == 0 + @test length(curric.courses[i].requisites[1]) == 0 elseif c.id == 11 @test curric.courses[i].name == "Technical Elective" @test curric.courses[i].credit_hours == 3 @test curric.courses[i].institution == "ACME State University" - @test length(curric.courses[i].requisites) == 0 + @test length(curric.courses[i].requisites[1]) == 0 elseif c.id == 12 @test curric.courses[i].name == "General Elective" @test curric.courses[i].credit_hours == 3 @test curric.courses[i].institution == "ACME State University" - @test length(curric.courses[i].requisites) == 0 + @test length(curric.courses[i].requisites[1]) == 0 end end # TODO: add learning outcomes @@ -132,7 +132,7 @@ for (i,c) in enumerate(curric.courses) @test curric.courses[i].num == "110" @test curric.courses[i].institution == "ACME State University" @test curric.courses[i].canonical_name == "Baskets I" - @test length(curric.courses[i].requisites) == 0 + @test length(curric.courses[i].requisites[1]) == 0 elseif c.id == 2 @test curric.courses[i].name == "Swimming" @test curric.courses[i].credit_hours == 3 @@ -140,7 +140,7 @@ for (i,c) in enumerate(curric.courses) @test curric.courses[i].num == "115" @test curric.courses[i].institution == "ACME State University" @test curric.courses[i].canonical_name == "Physical Education" - @test length(curric.courses[i].requisites) == 0 + @test length(curric.courses[i].requisites[1]) == 0 elseif c.id == 3 @test curric.courses[i].name == "Introductory Calculus w/ Basketry Applications" @test curric.courses[i].credit_hours == 4 @@ -149,7 +149,7 @@ for (i,c) in enumerate(curric.courses) @test curric.courses[i].institution == "ACME State University" @test curric.courses[i].canonical_name == "Calculus I" # this is the only difference from above tests - @test curric.courses[i].requisites[13] == pre + @test curric.courses[i].requisites[1][13] == pre elseif c.id == 4 @test curric.courses[i].name == "Basic Basket Forms" @test curric.courses[i].credit_hours == 3 @@ -157,8 +157,8 @@ for (i,c) in enumerate(curric.courses) @test curric.courses[i].num == "111" @test curric.courses[i].institution == "ACME State University" @test curric.courses[i].canonical_name == "Baskets II" - @test curric.courses[i].requisites[1] == pre - @test curric.courses[i].requisites[5] == strict_co + @test curric.courses[i].requisites[1][1] == pre + @test curric.courses[i].requisites[1][5] == strict_co elseif c.id == 5 @test curric.courses[i].name == "Basic Basket Forms Lab" @test curric.courses[i].credit_hours == 1 @@ -166,7 +166,7 @@ for (i,c) in enumerate(curric.courses) @test curric.courses[i].num == "111L" @test curric.courses[i].institution == "ACME State University" @test curric.courses[i].canonical_name == "Baskets II Laboratory" - @test length(curric.courses[i].requisites) == 0 + @test length(curric.courses[i].requisites[1]) == 0 elseif c.id == 6 @test curric.courses[i].name == "Advanced Basketry" @test curric.courses[i].credit_hours == 3 @@ -174,9 +174,9 @@ for (i,c) in enumerate(curric.courses) @test curric.courses[i].num == "201" @test curric.courses[i].institution == "ACME State University" @test curric.courses[i].canonical_name == "Baskets III" - @test curric.courses[i].requisites[4] == pre - @test curric.courses[i].requisites[5] == pre - @test curric.courses[i].requisites[3] == co + @test curric.courses[i].requisites[1][4] == pre + @test curric.courses[i].requisites[1][5] == pre + @test curric.courses[i].requisites[1][3] == co elseif c.id == 7 @test curric.courses[i].name == "Basket Materials & Decoration" @test curric.courses[i].credit_hours == 3 @@ -184,7 +184,7 @@ for (i,c) in enumerate(curric.courses) @test curric.courses[i].num == "214" @test curric.courses[i].institution == "ACME State University" @test curric.courses[i].canonical_name == "Basket Materials" - @test curric.courses[i].requisites[1] == pre + @test curric.courses[i].requisites[1][1] == pre elseif c.id == 8 @test curric.courses[i].name == "Underwater Weaving" @test curric.courses[i].credit_hours == 3 @@ -192,28 +192,28 @@ for (i,c) in enumerate(curric.courses) @test curric.courses[i].num == "301" @test curric.courses[i].institution == "ACME State University" @test curric.courses[i].canonical_name == "Baskets IV" - @test curric.courses[i].requisites[2] == pre - @test curric.courses[i].requisites[7] == co + @test curric.courses[i].requisites[1][2] == pre + @test curric.courses[i].requisites[1][7] == co elseif c.id == 9 @test curric.courses[i].name == "Humanitites Elective" @test curric.courses[i].credit_hours == 3 @test curric.courses[i].institution == "ACME State University" - @test length(curric.courses[i].requisites) == 0 + @test length(curric.courses[i].requisites[1]) == 0 elseif c.id == 10 @test curric.courses[i].name == "Social Sciences Elective" @test curric.courses[i].credit_hours == 3 @test curric.courses[i].institution == "ACME State University" - @test length(curric.courses[i].requisites) == 0 + @test length(curric.courses[i].requisites[1]) == 0 elseif c.id == 11 @test curric.courses[i].name == "Technical Elective" @test curric.courses[i].credit_hours == 3 @test curric.courses[i].institution == "ACME State University" - @test length(curric.courses[i].requisites) == 0 + @test length(curric.courses[i].requisites[1]) == 0 elseif c.id == 12 @test curric.courses[i].name == "General Elective" @test curric.courses[i].credit_hours == 3 @test curric.courses[i].institution == "ACME State University" - @test length(curric.courses[i].requisites) == 0 + @test length(curric.courses[i].requisites[1]) == 0 end end # test additional courses @@ -225,7 +225,7 @@ for (i,c) in enumerate(dp.additional_courses) @test dp.additional_courses[i].num == "110" @test dp.additional_courses[i].institution == "ACME State University" @test dp.additional_courses[i].canonical_name == "Precalculus" - @test dp.additional_courses[i].requisites[14] == pre + @test dp.additional_courses[i].requisites[1][14] == pre elseif c.id == 14 @test dp.additional_courses[i].name == "College Algebra" @test dp.additional_courses[i].credit_hours == 3 @@ -233,7 +233,7 @@ for (i,c) in enumerate(dp.additional_courses) @test dp.additional_courses[i].num == "102" @test dp.additional_courses[i].institution == "ACME State University" @test dp.additional_courses[i].canonical_name == "College Algebra" - @test dp.additional_courses[i].requisites[15] == strict_co + @test dp.additional_courses[i].requisites[1][15] == strict_co elseif c.id == 15 @test dp.additional_courses[i].name == "College Algebra Studio" @test dp.additional_courses[i].credit_hours == 1 @@ -241,7 +241,7 @@ for (i,c) in enumerate(dp.additional_courses) @test dp.additional_courses[i].num == "102S" @test dp.additional_courses[i].institution == "ACME State University" @test dp.additional_courses[i].canonical_name == "College Algebra Recitation" - @test length(dp.additional_courses[i].requisites) == 0 + @test length(dp.additional_courses[i].requisites[1]) == 0 elseif c.id == 16 @test dp.additional_courses[i].name == "Hemp Baskets" @test dp.additional_courses[i].credit_hours == 3 @@ -249,7 +249,7 @@ for (i,c) in enumerate(dp.additional_courses) @test dp.additional_courses[i].num == "420" @test dp.additional_courses[i].institution == "ACME State University" @test dp.additional_courses[i].canonical_name == "College Algebra Recitation" - @test dp.additional_courses[i].requisites[6] == co + @test dp.additional_courses[i].requisites[1][6] == co end end # TODO: add learning outcomes diff --git a/test/DataTypes.jl b/test/DataTypes.jl index 03621926..22c1b361 100644 --- a/test/DataTypes.jl +++ b/test/DataTypes.jl @@ -29,16 +29,16 @@ include("test_degree_plan.jl") @test course_id(A.prefix, A.num, A.name, A.institution) == convert(Int, mod(hash(A.name * A.prefix * A.num * A.institution), UInt32)) # Test add_requisite! function -@test length(A.requisites) == 0 -@test length(B.requisites) == 0 -@test length(C.requisites) == 3 -@test length(D.requisites) == 0 -@test length(E.requisites) == 1 -@test length(F.requisites) == 1 +@test length(A.requisites[1]) == 0 +@test length(B.requisites[1]) == 0 +@test length(C.requisites[1]) == 3 +@test length(D.requisites[1]) == 0 +@test length(E.requisites[1]) == 1 +@test length(F.requisites[1]) == 1 # Test delete_requisite! function delete_requisite!(A,C); -@test length(C.requisites) == 2 +@test length(C.requisites[1]) == 2 add_requisite!(A,C,pre); # Test Curriciulum creation From 279aac29092c28d4f776141b75275a0931331ab6 Mon Sep 17 00:00:00 2001 From: Greg Heileman Date: Mon, 18 Sep 2023 09:39:47 -0700 Subject: [PATCH 02/13] New requisite data struct and add_requisite_clause!() --- src/CurricularAnalytics.jl | 2 +- src/DataTypes/Course.jl | 49 ++++++++++++++++++++++++++++---------- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/CurricularAnalytics.jl b/src/CurricularAnalytics.jl index cfd03ee3..08767979 100644 --- a/src/CurricularAnalytics.jl +++ b/src/CurricularAnalytics.jl @@ -28,7 +28,7 @@ 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, + add_course!, add_lo_requisite!, add_requisite!, add_requisite_clause!, 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, diff --git a/src/DataTypes/Course.jl b/src/DataTypes/Course.jl index 7a09444b..27f08c5c 100644 --- a/src/DataTypes/Course.jl +++ b/src/DataTypes/Course.jl @@ -149,14 +149,19 @@ Required: - `tc::AbstractCourse` : target course, i.e., course for which `rc` is a requisite. - `requisite_type::Requisite` : requisite type. +# Keyword Arguments +- `clause::Int` : specify the DNF clause the requisite should appear in. + + # 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`. """ -function add_requisite!(requisite_course::AbstractCourse, course::AbstractCourse, requisite_type::Requisite) - course.requisites[1][requisite_course.id] = requisite_type +function add_requisite!(requisite_course::AbstractCourse, course::AbstractCourse, requisite_type::Requisite; clause::Int=1) + @assert clause <= length(course.requisites) + course.requisites[clause][requisite_course.id] = requisite_type end """ @@ -170,25 +175,23 @@ Required: - `tc::AbstractCourse` : target course, i.e., course for which `rc` is a requisite. - `requisite_type::Array{Requisite}` : an array of requisite types. +# Keyword Arguments +- `clause::Int` : specify the DNF clause the requisites should appear in (default = 1). + # 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`. """ -function add_requisite!(requisite_courses::Array{AbstractCourse}, course::AbstractCourse, requisite_types::Array{Requisite}) +function add_requisite!(requisite_courses::Array{AbstractCourse}, course::AbstractCourse, requisite_types::Array{Requisite}; clause::Int=1) + @assert clause <= length(course.requisites) @assert length(requisite_courses) == length(requisite_types) for i = 1:length(requisite_courses) - course.requisites[1][requisite_courses[i].id] = requisite_types[i] + course.requisites[clause][requisite_courses[i].id] = requisite_types[i] end end - -function add_requisite!(requisite_course::AbstractCourse, course::AbstractCourse, clause::Int, requisite_type::Requisite) - -end - - """ delete_requisite!(rc, tc) @@ -200,10 +203,32 @@ Required: - `rc::AbstractCourse` : requisite course. - `tc::AbstractCourse` : target course, i.e., course for which `rc` is a requisite. +# Keyword Arguments +- `clause::Int` : the DNF clause the requisite should be deleted from (default = 1). + """ -function delete_requisite!(requisite_course::Course, course::Course) +function delete_requisite!(requisite_course::Course, course::Course; clause::Int=1) #if !haskey(course.requisites, requisite_course.id) # error("The requisite you are trying to delete does not exist") #end - delete!(course.requisites[1], requisite_course.id) + @assert clause <= length(course.requisites) + delete!(course.requisites[clause], requisite_course.id) end + +""" + add_requisite_clause!(course::AbstractCourse) + +Add a clause to course's requisite formula. The clauses are stored in an array represent the clauses in a DNF expression. +The clause number (i.e., index) in the requisite array is returned. + +# Arguments +Required: +- `c::AbstractCourse` : target course. + +""" +function add_requisite_clause!(course::AbstractCourse) + return length(push!(course.requisites, Dict{Int, Requisite}())) +end + +# TODO: Create a version of add_requisite_clasue! that has an initial set of requisites (similar to add_requisite!) +# TODO: Add a delete_requisite_clause function. Issue: how do you specify the clause, by index, or some other search method? From bdf4ec12872c757dde2f453f08c7e61dabf6919f Mon Sep 17 00:00:00 2001 From: Greg Heileman Date: Mon, 18 Sep 2023 13:47:21 -0700 Subject: [PATCH 03/13] add comment --- src/DataTypes/Course.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DataTypes/Course.jl b/src/DataTypes/Course.jl index 27f08c5c..09406943 100644 --- a/src/DataTypes/Course.jl +++ b/src/DataTypes/Course.jl @@ -56,7 +56,7 @@ mutable struct Course <: AbstractCourse 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::Array{Dict{Int, Requisite},1} # Array of requisite clauses, each clause in (requisite_course id, requisite_type) format + requisites::Array{Dict{Int, Requisite},1} # Array of requisite clauses, each clause in (requisite_course id, requisite_type) format is assumed to be clause in a DNF formula. 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 From e60ec02c8d455974b4335bdc478c58bde402e8a8 Mon Sep 17 00:00:00 2001 From: Greg Heileman Date: Mon, 18 Sep 2023 16:05:57 -0700 Subject: [PATCH 04/13] remove code coverage 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 ---- 17 files changed, 3754 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 diff --git a/src/CSVUtilities.jl.33665.cov b/src/CSVUtilities.jl.33665.cov deleted file mode 100644 index f9287ebe..00000000 --- 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 e944ff7c..00000000 --- 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 366fcb28..00000000 --- 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 af8721b0..00000000 --- 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 0d057dde..00000000 --- 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 26d7dbfc..00000000 --- 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 8abfcf9f..00000000 --- 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 4ece750d..00000000 --- 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 fa3833a6..00000000 --- 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 c16267c6..00000000 --- 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 418d66ed..00000000 --- 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 6d136592..00000000 --- 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 27fbfae8..00000000 --- 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 43860a98..00000000 --- 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 f6103273..00000000 --- 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 993242af..00000000 --- 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 7e3f69e2..00000000 --- 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 From a4681fe39ef90bd63e0a08fefd1d647b47254408 Mon Sep 17 00:00:00 2001 From: Greg Heileman Date: Mon, 18 Sep 2023 17:21:54 -0700 Subject: [PATCH 05/13] add requisite_clause to Curriculum --- src/DataTypes/Course.jl | 10 ++++---- src/DataTypes/Curriculum.jl | 48 ++++++++++++++++--------------------- 2 files changed, 25 insertions(+), 33 deletions(-) diff --git a/src/DataTypes/Course.jl b/src/DataTypes/Course.jl index 09406943..208e0b08 100644 --- a/src/DataTypes/Course.jl +++ b/src/DataTypes/Course.jl @@ -56,7 +56,7 @@ mutable struct Course <: AbstractCourse 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::Array{Dict{Int, Requisite},1} # Array of requisite clauses, each clause in (requisite_course id, requisite_type) format is assumed to be clause in a DNF formula. + requisites::Array{Dict{Int, Requisite}, 1} # Array of requisite clauses, each clause in (requisite_course id, requisite_type) format is assumed to be clause in a DNF formula. 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 @@ -82,8 +82,7 @@ mutable struct Course <: AbstractCourse this.department = department this.cross_listed = cross_listed this.canonical_name = canonical_name - this.requisites = Array{Dict{Int, Requisite},1}() - push!(this.requisites, Dict{Int, Requisite}()) # create an empty first clause for requisites + this.requisites = fill(Dict{Int, Requisite}(), 1) # create an empty first DNF clause for requisites this.metrics = Dict{String, Any}() this.metadata = Dict{String, Any}() this.learning_outcomes = learning_outcomes @@ -105,7 +104,7 @@ mutable struct CourseCollection <: AbstractCourse 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::Array{Dict{Int, Requisite},1} # Array of requisite clauses, each clause in (requisite_course id, requisite_type) format + requisites::Array{Dict{Int, Requisite}, 1} # Array of requisite clauses, each clause in (requisite_course id, requisite_type) format metrics::Dict{String, Any} # Course-related metrics metadata::Dict{String, Any} # Course-related metadata @@ -125,8 +124,7 @@ mutable struct CourseCollection <: AbstractCourse this.college = college this.department = department this.canonical_name = canonical_name - this.requisites = Array{Dict{Int, Requisite},1}() - push!(this.requisites, Dict{Int, Requisite}()) # create an empty first clause for requisites + this.requisites = fill(Dict{Int, Requisite}(), 1) # create an empty first DNF clause for requisites this.metrics = Dict{String, Any}() this.metadata = Dict{String, Any}() this.vertex_id = Dict{Int, Int}() # curriculum id -> vertex id diff --git a/src/DataTypes/Curriculum.jl b/src/DataTypes/Curriculum.jl index 9c2182f6..c4be5763 100644 --- a/src/DataTypes/Curriculum.jl +++ b/src/DataTypes/Curriculum.jl @@ -29,17 +29,17 @@ 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 + 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 + courses::Array{AbstractCourse} # Array of required courses in curriculum + requisite_clauses::Dict{Course, Int} # Dictionary of the requisite clause that should be applied to each course in the 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 + 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 @@ -70,18 +70,18 @@ mutable struct Curriculum end this.num_courses = length(this.courses) this.credit_hours = total_credits(this) - this.graph = SimpleDiGraph{Int}() - create_graph!(this) this.metrics = Dict{String, Any}() this.metadata = Dict{String, Any}() - #= - requisite_clauses = requisite_clauses - if length(requisite_clauses == 0) - for c in this.courses - requisite_clauses[c] = 1 + this.requisite_clauses = requisite_clauses + for c ∈ this.courses + if c ∉ keys(this.requisite_clauses) + this.requisite_clauses[c] = 1 # if a requisite clause is not specificed for a course, set it to 1 + else # a clause was specified, make sure it exists in the course + @assert requisite_clauses[c] <= length(c.requisites) end end - =# + this.graph = SimpleDiGraph{Int}() + create_graph!(this) this.learning_outcomes = learning_outcomes this.learning_outcome_graph = SimpleDiGraph{Int}() create_learning_outcome_graph!(this) @@ -135,7 +135,7 @@ function is_valid(c::Curriculum, error_msg::IOBuffer=IOBuffer()) # 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. for course in c.courses - for (k,r) in course.requisites[1] + for (k,r) in course.requisites[c.requisite_clauses[course]] if r == strict_co v_d = course_from_id(c,course.id).vertex_id[c.id] # destination vertex v_s = course_from_id(c,k).vertex_id[c.id] # source vertex @@ -188,9 +188,9 @@ function convert_ids(curriculum::Curriculum) c1.id = mod(hash(c1.name * c1.prefix * c1.num * c1.institution), UInt32) if old_id != c1.id for c2 in curriculum.courses - if old_id in keys(c2.requisites[1]) - add_requisite!(c1, c2, c2.requisites[1][old_id]) - delete!(c2.requisites[1], old_id) + if old_id in keys(c2.requisites[curriculum.requisite_clauses[c2]]) + add_requisite!(c1, c2, c2.requisites[curriculum.requisite_clauses[c2]][old_id]) + delete!(c2.requisites[curriculum.requisite_clauses[c2]], old_id) end end end @@ -276,7 +276,7 @@ function create_graph!(curriculum::Curriculum) end mapped_vertex_ids = map_vertex_ids(curriculum) for c in curriculum.courses - for r in collect(keys(c.requisites[1])) + for r in collect(keys(c.requisites[curriculum.requisite_clauses[c]])) if add_edge!(curriculum.graph, mapped_vertex_ids[r], c.vertex_id[curriculum.id]) else s = course_from_id(curriculum, r) @@ -308,7 +308,6 @@ function create_course_learning_outcome_graph!(curriculum::Curriculum) end end - for (j, lo) in enumerate(curriculum.learning_outcomes) if add_vertex!(curriculum.course_learning_outcome_graph) lo.vertex_id[curriculum.id] = len_courses + j # The vertex id of a learning outcome w/in the curriculum @@ -318,16 +317,13 @@ function create_course_learning_outcome_graph!(curriculum::Curriculum) error("vertex could not be created") end end - mapped_vertex_ids = map_vertex_ids(curriculum) mapped_lo_vertex_ids = map_lo_vertex_ids(curriculum) - - # Add edges among courses for c in curriculum.courses - for r in collect(keys(c.requisites[1])) + for r in collect(keys(c.requisites[curriculum.requisite_clauses[c]])) if add_edge!(curriculum.course_learning_outcome_graph, mapped_vertex_ids[r], c.vertex_id[curriculum.id]) - set_prop!(curriculum.course_learning_outcome_graph, Edge(mapped_vertex_ids[r], c.vertex_id[curriculum.id]), :c_to_c, c.requisites[1][r]) + set_prop!(curriculum.course_learning_outcome_graph, Edge(mapped_vertex_ids[r], c.vertex_id[curriculum.id]), :c_to_c, c.requisites[curriculum.requisite_clauses[c]][r]) else s = course_from_id(curriculum, r) @@ -335,7 +331,6 @@ function create_course_learning_outcome_graph!(curriculum::Curriculum) end end end - # Add edges among learning_outcomes for lo in curriculum.learning_outcomes for r in collect(keys(lo.requisites)) @@ -347,7 +342,6 @@ function create_course_learning_outcome_graph!(curriculum::Curriculum) end end end - # Add edges between each pair of a course and a learning outcome for c in curriculum.courses for lo in c.learning_outcomes @@ -399,9 +393,9 @@ function requisite_type(curriculum::Curriculum, src_course_id::Int, dst_course_i dst = c end end - if ((src == 0 || dst == 0) || !haskey(dst.requisites[1], src.id)) + if ((src == 0 || dst == 0) || !haskey(dst.requisites[curriculum.requisite_clauses[dst]], src.id)) error("edge ($src_course_id, $dst_course_id) does not exist in curriculum graph") else - return dst.requisites[1][src.id] + return dst.requisites[curriculum.requisite_clauses[dst]][src.id] end end \ No newline at end of file From 5230ee761e41a2929babace1dae85bdad3f96d0b Mon Sep 17 00:00:00 2001 From: Greg Heileman Date: Tue, 19 Sep 2023 18:41:17 -0700 Subject: [PATCH 06/13] requisite_clauses work, except in DegreePlanCreation.jl and merge_curricula() --- src/CSVUtilities.jl | 1 + src/CurricularAnalytics.jl | 3 ++- src/DataTypes/DegreePlan.jl | 6 +++--- src/DegreePlanAnalytics.jl | 2 +- src/DegreePlanCreation.jl | 3 ++- src/Simulation/Enrollment.jl | 2 -- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/CSVUtilities.jl b/src/CSVUtilities.jl index 82900746..d4bdf9be 100644 --- a/src/CSVUtilities.jl +++ b/src/CSVUtilities.jl @@ -38,6 +38,7 @@ function find_courses(courses, course_id) return false end +#TODO need to pass in a curriculum in order to access the requisite_clauses array function course_line(course, term_id; metrics=false) course_ID = course.id course_name = course.name diff --git a/src/CurricularAnalytics.jl b/src/CurricularAnalytics.jl index 08767979..ff669062 100644 --- a/src/CurricularAnalytics.jl +++ b/src/CurricularAnalytics.jl @@ -77,7 +77,7 @@ function extraneous_requisites(c::Curriculum; print=false) remove = true for n in nb # check for co- or strict_co requisites if has_path(c.graph, n, v) # is there a path from n to v? - req_type = c.courses[n].requisites[1][c.courses[u].id] # the requisite relationship between u and n + req_type = c.courses[n].requisites[c.requisite_clauses[c.courses[n]]][c.courses[u].id] # the requisite relationship between u and n if (req_type == co) || (req_type == strict_co) # is u a co or strict_co requisite for n? remove = false # a co or strict_co relationshipo is involved, must keep (u, v) end @@ -629,6 +629,7 @@ courses must be identical (at the level of memory allocation). Allowable match c - `credit hours` : the course credit hours must be indentical. """ +#TODO Integrate use of requisite_clauses into this function 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="") diff --git a/src/DataTypes/DegreePlan.jl b/src/DataTypes/DegreePlan.jl index 6f2c4265..20591231 100644 --- a/src/DataTypes/DegreePlan.jl +++ b/src/DataTypes/DegreePlan.jl @@ -127,7 +127,7 @@ function is_valid(plan::DegreePlan, error_msg::IOBuffer=IOBuffer()) for c in plan.terms[i].courses for j in i-1:-1:1 for k in plan.terms[j].courses - for l in keys(k.requisites[1]) + for l in keys(k.requisites[plan.curriculum.requisite_clauses[k]]) if l == c.id validity = false write(error_msg, "\n-Invalid requisite: $(c.name) in term $i is a requisite for $(k.name) in term $j") @@ -143,8 +143,8 @@ function is_valid(plan::DegreePlan, error_msg::IOBuffer=IOBuffer()) for r in plan.terms[i].courses if c == r continue - elseif haskey(c.requisites[1], r.id) - if c.requisites[1][r.id] == pre + elseif haskey(c.requisites[plan.curriculum.requisite_clauses[c]], r.id) + if c.requisites[plan.curriculum.requisite_clauses[c]][r.id] == pre validity = false write(error_msg, "\n-Invalid prerequisite: $(r.name) in term $i is a prerequisite for $(c.name) in the same term") end diff --git a/src/DegreePlanAnalytics.jl b/src/DegreePlanAnalytics.jl index 8c2cfa41..9dfcd016 100644 --- a/src/DegreePlanAnalytics.jl +++ b/src/DegreePlanAnalytics.jl @@ -102,7 +102,7 @@ The requisite distance metric computed by this function is stored in the associa function requisite_distance(plan::DegreePlan, course::Course) distance = 0 term = find_term(plan, course) - for req in keys(course.requisites[1]) + for req in keys(course.requisites[plan.curriculum.requisite_clauses[course]]) distance = distance + (term - find_term(plan, course_from_id(plan.curriculum, req))) end return course.metrics["requisite distance"] = distance diff --git a/src/DegreePlanCreation.jl b/src/DegreePlanCreation.jl index 2b33e853..6ed07e5a 100644 --- a/src/DegreePlanCreation.jl +++ b/src/DegreePlanCreation.jl @@ -29,7 +29,7 @@ function bin_filling(curric::Curriculum, additional_courses::Array{AbstractCours end # if c serves as a strict-corequisite for other courses, include them in current term too for course in UC - for req in course.requisites[1] + for req in course.requisites[1] #TODO this does not work using: course.requisites[curric.requisite_clauses[course]] if req[1] == c.id if req[2] == strict_co deleteat!(UC, findfirst(isequal(course), UC)) @@ -66,6 +66,7 @@ function select_vertex(curric::Curriculum, term_courses::Array{AbstractCourse,1} if invariant1 == true invariant2 = true for c in term_courses + #TODO the line below does not work using: target.requisites[curric.requisite_clauses[target]] if c.id in collect(keys(target.requisites[1])) && target.requisites[1][c.id] == pre # AND shortcircuits, otherwise 2nd expression would error invariant2 = false break # try a new target diff --git a/src/Simulation/Enrollment.jl b/src/Simulation/Enrollment.jl index 5151804d..87fddb93 100644 --- a/src/Simulation/Enrollment.jl +++ b/src/Simulation/Enrollment.jl @@ -55,8 +55,6 @@ module Enrollment end end - - # Determine wheter the student can be enrolled in the current course. if canEnroll(student, course, courses, student_progress, max_credits, current_term) From e96b2ed30676730c54b3dafe6fd0127ca52ed816 Mon Sep 17 00:00:00 2001 From: Greg Heileman Date: Thu, 21 Sep 2023 09:18:53 -0700 Subject: [PATCH 07/13] edit comments --- src/DataTypes/Curriculum.jl | 4 ++-- src/DegreePlanCreation.jl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/DataTypes/Curriculum.jl b/src/DataTypes/Curriculum.jl index c4be5763..cf692f2f 100644 --- a/src/DataTypes/Curriculum.jl +++ b/src/DataTypes/Curriculum.jl @@ -74,9 +74,9 @@ mutable struct Curriculum this.metadata = Dict{String, Any}() this.requisite_clauses = requisite_clauses for c ∈ this.courses - if c ∉ keys(this.requisite_clauses) + if c ∉ keys(this.requisite_clauses) this.requisite_clauses[c] = 1 # if a requisite clause is not specificed for a course, set it to 1 - else # a clause was specified, make sure it exists in the course + else # a clause was specified, make sure it exists in the array of requisite clauses @assert requisite_clauses[c] <= length(c.requisites) end end diff --git a/src/DegreePlanCreation.jl b/src/DegreePlanCreation.jl index 6ed07e5a..efcd6c15 100644 --- a/src/DegreePlanCreation.jl +++ b/src/DegreePlanCreation.jl @@ -15,7 +15,7 @@ function bin_filling(curric::Curriculum, additional_courses::Array{AbstractCours terms = Array{Term,1}() term_credits = 0 term_courses = Array{AbstractCourse,1}() - UC = sort!(deepcopy(curric.courses), by=course_num) # lower numbered courses will be considered first + UC = sort!(deepcopy(curric.courses), by=course_num) # course_num is a function, lower numbered courses will be considered first while length(UC) > 0 if ((c = select_vertex(curric, term_courses, UC)) != nothing) deleteat!(UC, findfirst(isequal(c), UC)) From d8826a77d44831f3fb65a317df2e6bfeb4c14a95 Mon Sep 17 00:00:00 2001 From: Greg Heileman Date: Thu, 21 Sep 2023 16:01:42 -0700 Subject: [PATCH 08/13] use course id in requisite_clauses (not course) --- src/CurricularAnalytics.jl | 2 +- src/DataTypes/Curriculum.jl | 39 ++-- src/DataTypes/DegreePlan.jl | 6 +- src/DegreePlanAnalytics.jl | 2 +- src/DegreePlanCreation.jl | 5 +- test/DataHandler.jl.33665.cov | 305 --------------------------- test/DegreePlanCreation.jl.33665.cov | 75 ------- 7 files changed, 26 insertions(+), 408 deletions(-) delete mode 100644 test/DataHandler.jl.33665.cov delete mode 100644 test/DegreePlanCreation.jl.33665.cov diff --git a/src/CurricularAnalytics.jl b/src/CurricularAnalytics.jl index ff669062..716dc424 100644 --- a/src/CurricularAnalytics.jl +++ b/src/CurricularAnalytics.jl @@ -77,7 +77,7 @@ function extraneous_requisites(c::Curriculum; print=false) remove = true for n in nb # check for co- or strict_co requisites if has_path(c.graph, n, v) # is there a path from n to v? - req_type = c.courses[n].requisites[c.requisite_clauses[c.courses[n]]][c.courses[u].id] # the requisite relationship between u and n + req_type = c.courses[n].requisites[c.requisite_clauses[c.courses[n].id]][c.courses[u].id] # the requisite relationship between u and n if (req_type == co) || (req_type == strict_co) # is u a co or strict_co requisite for n? remove = false # a co or strict_co relationshipo is involved, must keep (u, v) end diff --git a/src/DataTypes/Curriculum.jl b/src/DataTypes/Curriculum.jl index cf692f2f..561fbe57 100644 --- a/src/DataTypes/Curriculum.jl +++ b/src/DataTypes/Curriculum.jl @@ -33,7 +33,7 @@ mutable struct Curriculum system_type::System # Semester or quarter system CIP::AbstractString # CIP code associated with the curriculum courses::Array{AbstractCourse} # Array of required courses in curriculum - requisite_clauses::Dict{Course, Int} # Dictionary of the requisite clause that should be applied to each course in the curriculum + requisite_clauses::Dict{Int, Int} # Dictionary of the requisite clause that should be applied to each course in the curriculum, in (course_id, clause #) format 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 @@ -46,10 +46,7 @@ mutable struct Curriculum metadata::Dict{String, Any} # Curriculum-related metadata # Constructor - #function Curriculum(name::AbstractString, courses::Array{AbstractCourse}; requisite_clauses::Dict{Course, Int}=Dict{Course, Int}(), 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) - function Curriculum(name::AbstractString, courses::Array{AbstractCourse}; requisite_clauses::Dict{Course, Int}=Dict{Course, Int}(), learning_outcomes::Array{LearningOutcome}=Array{LearningOutcome,1}(), + function Curriculum(name::AbstractString, courses::Array{AbstractCourse}; requisite_clauses::Dict{Int, Int}=Dict{Int, Int}(), 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) this = new() @@ -74,10 +71,10 @@ mutable struct Curriculum this.metadata = Dict{String, Any}() this.requisite_clauses = requisite_clauses for c ∈ this.courses - if c ∉ keys(this.requisite_clauses) - this.requisite_clauses[c] = 1 # if a requisite clause is not specificed for a course, set it to 1 - else # a clause was specified, make sure it exists in the array of requisite clauses - @assert requisite_clauses[c] <= length(c.requisites) + if c.id ∉ keys(this.requisite_clauses) + this.requisite_clauses[c.id] = 1 # if a requisite clause is not specificed for a course, set it to 1 + else # a clause number was specified for a course in a curriculum, make sure are at least that many requisite clauses in the course + @assert requisite_clauses[c.id] <= length(c.requisites) end end this.graph = SimpleDiGraph{Int}() @@ -95,7 +92,7 @@ mutable struct Curriculum return this end - function Curriculum(name::AbstractString, courses::Array{Course}; requisite_clauses::Dict{Course, Int}=Dict{Course, Int}(), + function Curriculum(name::AbstractString, courses::Array{Course}; requisite_clauses::Dict{Int, Int}=Dict{Int, Int}(), 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) Curriculum(name, convert(Array{AbstractCourse},courses), requisite_clauses=requisite_clauses, learning_outcomes=learning_outcomes, @@ -135,7 +132,7 @@ function is_valid(c::Curriculum, error_msg::IOBuffer=IOBuffer()) # 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. for course in c.courses - for (k,r) in course.requisites[c.requisite_clauses[course]] + for (k,r) in course.requisites[c.requisite_clauses[course.id]] if r == strict_co v_d = course_from_id(c,course.id).vertex_id[c.id] # destination vertex v_s = course_from_id(c,k).vertex_id[c.id] # source vertex @@ -187,10 +184,12 @@ function convert_ids(curriculum::Curriculum) old_id = c1.id c1.id = mod(hash(c1.name * c1.prefix * c1.num * c1.institution), UInt32) if old_id != c1.id - for c2 in curriculum.courses - if old_id in keys(c2.requisites[curriculum.requisite_clauses[c2]]) - add_requisite!(c1, c2, c2.requisites[curriculum.requisite_clauses[c2]][old_id]) - delete!(c2.requisites[curriculum.requisite_clauses[c2]], old_id) + for c2 in curriculum.courses # must change the id in all of the requisites + for (i, clause) in enumerate(c2.requisites) # check each clause in the DNF, even if it's not being used in the curriculum + if old_id in keys(clause) + add_requisite!(c1, c2, clause[old_id], clause=i) + delete!(clause, old_id) #TODO: why are we not using delete_requisite!() + end end end end @@ -276,7 +275,7 @@ function create_graph!(curriculum::Curriculum) end mapped_vertex_ids = map_vertex_ids(curriculum) for c in curriculum.courses - for r in collect(keys(c.requisites[curriculum.requisite_clauses[c]])) + for r in collect(keys(c.requisites[curriculum.requisite_clauses[c.id]])) if add_edge!(curriculum.graph, mapped_vertex_ids[r], c.vertex_id[curriculum.id]) else s = course_from_id(curriculum, r) @@ -321,9 +320,9 @@ function create_course_learning_outcome_graph!(curriculum::Curriculum) mapped_lo_vertex_ids = map_lo_vertex_ids(curriculum) # Add edges among courses for c in curriculum.courses - for r in collect(keys(c.requisites[curriculum.requisite_clauses[c]])) + for r in collect(keys(c.requisites[curriculum.requisite_clauses[c.id]])) if add_edge!(curriculum.course_learning_outcome_graph, mapped_vertex_ids[r], c.vertex_id[curriculum.id]) - set_prop!(curriculum.course_learning_outcome_graph, Edge(mapped_vertex_ids[r], c.vertex_id[curriculum.id]), :c_to_c, c.requisites[curriculum.requisite_clauses[c]][r]) + set_prop!(curriculum.course_learning_outcome_graph, Edge(mapped_vertex_ids[r], c.vertex_id[curriculum.id]), :c_to_c, c.requisites[curriculum.requisite_clauses[c.id]][r]) else s = course_from_id(curriculum, r) @@ -393,9 +392,9 @@ function requisite_type(curriculum::Curriculum, src_course_id::Int, dst_course_i dst = c end end - if ((src == 0 || dst == 0) || !haskey(dst.requisites[curriculum.requisite_clauses[dst]], src.id)) + if ((src == 0 || dst == 0) || !haskey(dst.requisites[curriculum.requisite_clauses[dst.id]], src.id)) error("edge ($src_course_id, $dst_course_id) does not exist in curriculum graph") else - return dst.requisites[curriculum.requisite_clauses[dst]][src.id] + return dst.requisites[curriculum.requisite_clauses[dst.id]][src.id] end end \ No newline at end of file diff --git a/src/DataTypes/DegreePlan.jl b/src/DataTypes/DegreePlan.jl index 20591231..f1a882cb 100644 --- a/src/DataTypes/DegreePlan.jl +++ b/src/DataTypes/DegreePlan.jl @@ -127,7 +127,7 @@ function is_valid(plan::DegreePlan, error_msg::IOBuffer=IOBuffer()) for c in plan.terms[i].courses for j in i-1:-1:1 for k in plan.terms[j].courses - for l in keys(k.requisites[plan.curriculum.requisite_clauses[k]]) + for l in keys(k.requisites[plan.curriculum.requisite_clauses[k.id]]) if l == c.id validity = false write(error_msg, "\n-Invalid requisite: $(c.name) in term $i is a requisite for $(k.name) in term $j") @@ -143,8 +143,8 @@ function is_valid(plan::DegreePlan, error_msg::IOBuffer=IOBuffer()) for r in plan.terms[i].courses if c == r continue - elseif haskey(c.requisites[plan.curriculum.requisite_clauses[c]], r.id) - if c.requisites[plan.curriculum.requisite_clauses[c]][r.id] == pre + elseif haskey(c.requisites[plan.curriculum.requisite_clauses[c.id]], r.id) + if c.requisites[plan.curriculum.requisite_clauses[c.id]][r.id] == pre validity = false write(error_msg, "\n-Invalid prerequisite: $(r.name) in term $i is a prerequisite for $(c.name) in the same term") end diff --git a/src/DegreePlanAnalytics.jl b/src/DegreePlanAnalytics.jl index 9dfcd016..4adefbdb 100644 --- a/src/DegreePlanAnalytics.jl +++ b/src/DegreePlanAnalytics.jl @@ -102,7 +102,7 @@ The requisite distance metric computed by this function is stored in the associa function requisite_distance(plan::DegreePlan, course::Course) distance = 0 term = find_term(plan, course) - for req in keys(course.requisites[plan.curriculum.requisite_clauses[course]]) + for req in keys(course.requisites[plan.curriculum.requisite_clauses[course.id]]) distance = distance + (term - find_term(plan, course_from_id(plan.curriculum, req))) end return course.metrics["requisite distance"] = distance diff --git a/src/DegreePlanCreation.jl b/src/DegreePlanCreation.jl index efcd6c15..11e84b23 100644 --- a/src/DegreePlanCreation.jl +++ b/src/DegreePlanCreation.jl @@ -29,7 +29,7 @@ function bin_filling(curric::Curriculum, additional_courses::Array{AbstractCours end # if c serves as a strict-corequisite for other courses, include them in current term too for course in UC - for req in course.requisites[1] #TODO this does not work using: course.requisites[curric.requisite_clauses[course]] + for req in course.requisites[curric.requisite_clauses[course.id]] if req[1] == c.id if req[2] == strict_co deleteat!(UC, findfirst(isequal(course), UC)) @@ -66,8 +66,7 @@ function select_vertex(curric::Curriculum, term_courses::Array{AbstractCourse,1} if invariant1 == true invariant2 = true for c in term_courses - #TODO the line below does not work using: target.requisites[curric.requisite_clauses[target]] - if c.id in collect(keys(target.requisites[1])) && target.requisites[1][c.id] == pre # AND shortcircuits, otherwise 2nd expression would error + if c.id in collect(keys(target.requisites[curric.requisite_clauses[target.id]])) && target.requisites[curric.requisite_clauses[target.id]][c.id] == pre # AND shortcircuits, otherwise 2nd expression would error invariant2 = false break # try a new target end diff --git a/test/DataHandler.jl.33665.cov b/test/DataHandler.jl.33665.cov deleted file mode 100644 index 8bebda33..00000000 --- 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 57650d4a..00000000 --- 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 From 59f1d32603c6e2263c039d4491ea2fbaff2bf0ab Mon Sep 17 00:00:00 2001 From: Hayden Free Date: Mon, 2 Oct 2023 21:34:21 -0400 Subject: [PATCH 09/13] Version 2.0.0 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index e26dde8d..697a8f17 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "CurricularAnalytics" uuid = "593ffa3d-269e-5d81-88bc-c3b6809c35a6" authors = ["Greg Heileman ", "Hayden Free "] -version = "1.5.0" +version = "2.0.0" [deps] CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" From 86995105d8614ac3b70a98237a90eba619f1e01f Mon Sep 17 00:00:00 2001 From: Greg Heileman Date: Wed, 29 Nov 2023 15:25:02 -0700 Subject: [PATCH 10/13] Update show_requirements() doc string --- src/RequirementsAnalytics.jl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/RequirementsAnalytics.jl b/src/RequirementsAnalytics.jl index 9d73ad6b..2acfb363 100644 --- a/src/RequirementsAnalytics.jl +++ b/src/RequirementsAnalytics.jl @@ -102,9 +102,8 @@ status of each degree requirement. Note: the `satisfied` function can be used t ```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) +julia> is_satisfied = satisfied(coalesce_transcript(transcript), flatten_requirements(program_requirements), value.(x), is_satisfied) +julia> show_requirements(program_requirements, satisfied=is_satisfied) ```` """ function show_requirements(root::AbstractRequirement; io::IO = stdout, tab = " ", From 99e9291abb04a255aed35cd28b57892548dd5d69 Mon Sep 17 00:00:00 2001 From: Greg Heileman Date: Fri, 1 Dec 2023 14:00:21 -0700 Subject: [PATCH 11/13] is_valid() check for CourseSets that are subsets --- src/DataTypes/Requirements.jl | 9 +++++++++ test/RequirementsAnalytics.jl | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/DataTypes/Requirements.jl b/src/DataTypes/Requirements.jl index f98e6909..10d42f0b 100644 --- a/src/DataTypes/Requirements.jl +++ b/src/DataTypes/Requirements.jl @@ -279,6 +279,15 @@ function is_valid(root::AbstractRequirement, error_msg::IOBuffer = IOBuffer()) validity = false 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 + # make sure the requirement set is not a proper subset of another requirement set + for r_comp in reqs + if typeof(r_comp) == CourseSet && r != r_comp # r_comp is a CourseSet and it is not r + if r.course_reqs ⊆ r_comp.course_reqs + validity = false + write(error_msg, "CourseSet: $(r.name) is a subset of $(r_comp.name), making $(r_comp.name) unnecessary.\n") + end + end + end else # r is a RequirementSet if (r.satisfy == 0) validity = false diff --git a/test/RequirementsAnalytics.jl b/test/RequirementsAnalytics.jl index 46f2812d..faec2d2a 100644 --- a/test/RequirementsAnalytics.jl +++ b/test/RequirementsAnalytics.jl @@ -82,10 +82,30 @@ lev = level(program_requirements) @test lev[2605601572] == 2 @test lev[2663541090] == 2 +# not enough credits in the sub-requirements to meet the 41 CH requirement bad_program_requirements = RequirementSet("Program Requirements", 41, program_reqs, description="Degree Requirements for the BS Program") @test is_valid(bad_program_requirements, errors) == false +# not enough credits in the collection of courses to meet 6 CHs bad_dr3 = CourseSet("Humanities Requirement", 6, humanities_courses, description="General Education Humanities Requirement", double_count = true) @test is_valid(bad_dr3, errors) == false +# duplicate requirement sets +dr_dup = RequirementSet("Another Gen. Ed. Core", 13, gen_ed_reqs, description="Identical General Education Requirements") +dup_reqs = RequirementSet("Two gen eds that are identical", 13, [dr4, dr_dup], description="Invalid Requirement Set") +@test is_valid(dup_reqs, errors) == false + +# duplication within requirement sets +gen_ed_reqs_subset = Array{AbstractRequirement,1}() +push!(gen_ed_reqs_subset, dr1, dr2) +subset_reqs = RequirementSet("Subset Gen. Ed. Core", 9, gen_ed_reqs_subset, description="Proper Subset") +bad_reqs_subset = RequirementSet("Overlapping Requirements", 13, [dr4, subset_reqs], description="Gen. Ed. w/ Proper Subset") +@test is_valid(bad_reqs_subset, errors) == false + +subset_courses = CourseSet("Subset Humanities Requirement", 1, [C4 => grade("D")], description="General Education Humanities Requirement subset", double_count = true) +program_reqs = Array{AbstractRequirement,1}() +push!(program_reqs, dr4, dr5, dr6, dr7, dr8, dr11, dr12, subset_courses) +program_requirements = RequirementSet("Program Requirements w/ subset courses", 40, program_reqs, description="Degree Requirements for the BS Program w/ subset courses") +@test is_valid(program_requirements, errors) == false + end \ No newline at end of file From ea0f06c0e833934e662245e5435e0223daf411d4 Mon Sep 17 00:00:00 2001 From: Greg Heileman Date: Wed, 6 Dec 2023 10:20:55 -0700 Subject: [PATCH 12/13] check for missing requisites in create_graph!() --- src/DataTypes/Curriculum.jl | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/DataTypes/Curriculum.jl b/src/DataTypes/Curriculum.jl index 561fbe57..bd4e1c72 100644 --- a/src/DataTypes/Curriculum.jl +++ b/src/DataTypes/Curriculum.jl @@ -276,10 +276,14 @@ function create_graph!(curriculum::Curriculum) mapped_vertex_ids = map_vertex_ids(curriculum) for c in curriculum.courses for r in collect(keys(c.requisites[curriculum.requisite_clauses[c.id]])) - if add_edge!(curriculum.graph, mapped_vertex_ids[r], c.vertex_id[curriculum.id]) - else - s = course_from_id(curriculum, r) - error("edge could not be created: ($(s.name), $(c.name))") + if r ∉ mapped_vertex_ids + warning("requisite $(r.name), required by $(c.name), is not in curriculum $(c.name)") + else + if add_edge!(curriculum.graph, mapped_vertex_ids[r], c.vertex_id[curriculum.id]) + else + s = course_from_id(curriculum, r) + error("edge could not be created: ($(s.name), $(c.name))") + end end end end @@ -288,8 +292,8 @@ 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. +#Create a curriculum directed graph from a curriculum specification. This graph contains courses and learning outcomes +# of the curriculum. The graph is stored as a Graph.jl implemenation within the Curriculum data object. #""" From 0aa2a67d5d4885d0636dec1fa8fed54bde953314 Mon Sep 17 00:00:00 2001 From: Greg Heileman Date: Wed, 6 Dec 2023 12:23:06 -0700 Subject: [PATCH 13/13] fix bug in create_graph!() --- src/DataTypes/Course.jl | 1 + src/DataTypes/Curriculum.jl | 8 ++++---- test/CurricularAnalytics.jl | 6 ++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/DataTypes/Course.jl b/src/DataTypes/Course.jl index 208e0b08..d5dd09b8 100644 --- a/src/DataTypes/Course.jl +++ b/src/DataTypes/Course.jl @@ -132,6 +132,7 @@ mutable struct CourseCollection <: AbstractCourse end end +# create a unique course id for a couse by hashing on the course's prefix, num, name, institution. function course_id(prefix::AbstractString, num::AbstractString, name::AbstractString, institution::AbstractString) convert(Int, mod(hash(name * prefix * num * institution), UInt32)) end diff --git a/src/DataTypes/Curriculum.jl b/src/DataTypes/Curriculum.jl index bd4e1c72..91f82985 100644 --- a/src/DataTypes/Curriculum.jl +++ b/src/DataTypes/Curriculum.jl @@ -81,9 +81,9 @@ mutable struct Curriculum create_graph!(this) this.learning_outcomes = learning_outcomes this.learning_outcome_graph = SimpleDiGraph{Int}() - create_learning_outcome_graph!(this) + #create_learning_outcome_graph!(this) this.course_learning_outcome_graph = MetaDiGraph() - create_course_learning_outcome_graph!(this) + #create_course_learning_outcome_graph!(this) errors = IOBuffer() if !(is_valid(this, errors)) printstyled("WARNING: Curriculum was created, but is invalid due to requisite cycle(s):", color = :yellow) @@ -276,8 +276,8 @@ function create_graph!(curriculum::Curriculum) mapped_vertex_ids = map_vertex_ids(curriculum) for c in curriculum.courses for r in collect(keys(c.requisites[curriculum.requisite_clauses[c.id]])) - if r ∉ mapped_vertex_ids - warning("requisite $(r.name), required by $(c.name), is not in curriculum $(c.name)") + if r ∉ keys(mapped_vertex_ids) + printstyled("WARNING: A requisite for course $(c.name) is not in curriculum $(curriculum.name)\n", color = :yellow) else if add_edge!(curriculum.graph, mapped_vertex_ids[r], c.vertex_id[curriculum.id]) else diff --git a/test/CurricularAnalytics.jl b/test/CurricularAnalytics.jl index 0ec4436e..74a888a0 100644 --- a/test/CurricularAnalytics.jl +++ b/test/CurricularAnalytics.jl @@ -147,6 +147,12 @@ errors = IOBuffer() @test is_valid(curric, errors) == true @test length(extraneous_requisites(curric)) == 0 +# Test case where the requisite for a course in a curriculum is not itself in the curriculum +H = Course("H", 3) +add_requisite!(H,G,pre) # H is a prerequiste to G, but not in the curriculum +curric = Curriculum("Postmodern Basket Weaving", [A,B,C,D,E,F,G], sortby_ID=false) +# this should issue a warning + # Test analytics @test delay_factor(curric) == (33.0, [5.0, 5.0, 5.0, 5.0, 3.0, 5.0, 5.0]) @test blocking_factor(curric) == (16, [6, 3, 4, 2, 0, 0, 1])