Skip to content

Commit

Permalink
Merge branch 'version_2' into development
Browse files Browse the repository at this point in the history
  • Loading branch information
haydenfree committed Jun 11, 2024
2 parents 1dd5acd + 0aa2a67 commit f24b98a
Show file tree
Hide file tree
Showing 13 changed files with 91 additions and 61 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "CurricularAnalytics"
uuid = "593ffa3d-269e-5d81-88bc-c3b6809c35a6"
authors = ["Greg Heileman <[email protected]>", "Hayden Free <[email protected]>"]
version = "1.5.0"
version = "2.0.0"

[deps]
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
Expand Down
1 change: 1 addition & 0 deletions src/CSVUtilities.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/CurricularAnalytics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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].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
Expand Down Expand Up @@ -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="")
Expand Down
11 changes: 5 additions & 6 deletions src/DataTypes/Course.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand All @@ -125,15 +124,15 @@ 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
return this
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
Expand Down
79 changes: 38 additions & 41 deletions src/DataTypes/Curriculum.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,27 +29,24 @@ 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{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
# 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
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()
Expand All @@ -70,23 +67,23 @@ 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.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}()
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)
Expand All @@ -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,
Expand Down Expand Up @@ -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[1]
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
Expand Down Expand Up @@ -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[1])
add_requisite!(c1, c2, c2.requisites[1][old_id])
delete!(c2.requisites[1], 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
Expand Down Expand Up @@ -276,11 +275,15 @@ 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]))
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))")
for r in collect(keys(c.requisites[curriculum.requisite_clauses[c.id]]))
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
s = course_from_id(curriculum, r)
error("edge could not be created: ($(s.name), $(c.name))")
end
end
end
end
Expand All @@ -289,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.


#"""
Expand All @@ -308,7 +311,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
Expand All @@ -318,24 +320,20 @@ 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.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[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.id]][r])

else
s = course_from_id(curriculum, r)
error("edge could not be created: ($(s.name), $(c.name))")
end
end
end

# Add edges among learning_outcomes
for lo in curriculum.learning_outcomes
for r in collect(keys(lo.requisites))
Expand All @@ -347,7 +345,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
Expand Down Expand Up @@ -399,9 +396,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.id]], 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.id]][src.id]
end
end
6 changes: 3 additions & 3 deletions src/DataTypes/DegreePlan.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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.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")
Expand All @@ -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.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
Expand Down
9 changes: 9 additions & 0 deletions src/DataTypes/Requirements.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/DegreePlanAnalytics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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.id]])
distance = distance + (term - find_term(plan, course_from_id(plan.curriculum, req)))
end
return course.metrics["requisite distance"] = distance
Expand Down
Loading

0 comments on commit f24b98a

Please sign in to comment.