diff --git a/docs/src/degreeplans.md b/docs/src/degreeplans.md index 6a9026af..5fe72c82 100644 --- a/docs/src/degreeplans.md +++ b/docs/src/degreeplans.md @@ -19,7 +19,7 @@ In order to be considered *minimally feasible*, a degree plan $P$ for a curricul The Curricular Analytics Toolbox also allows you to create customized degree plans according to various user-specifed criteria. These features make use of the [JuMP](https://github.com/JuliaOpt/JuMP.jl) domain-specific language for specifying optimization problems in Julia, and calls the [Gurobi](https://www.gurobi.com) solver in order to solve the optimzaton problems. In order to use these features you must first install JuMP and Gurobi. For installation instructions see [Additional Requirements](@ref) in the Installation section. -A brief overview of how we have structured the degree plan creation process as an optimzation problem is provided next. Assume a curriculum consisting of $n$ courses is organized over $m$ terms. The degree plan creation process involves a partitioning of the $n$ courses in a curriculum into $m$ disjoint sets. Thus, we can represent a degree plan an $n \times m$ binary-valued assignment matrix $x$, where +A brief overview of how we have structured the degree plan creation process as an optimization problem is provided next. Assume a curriculum consisting of $n$ courses is organized over $m$ terms. The degree plan creation process involves a partitioning of the $n$ courses in a curriculum into $m$ disjoint sets. Thus, we can represent a degree plan an $n \times m$ binary-valued assignment matrix $x$, where ```math x_{ij} = \left\{ @@ -37,7 +37,7 @@ The two conditions required for a degree plan to be minimally feasible can be ex \mbox{Constraint 1:} \ \ \sum_{j=1}^m x_{ij} = 1, \ \ \ \ i = 1 \ldots n. ``` -If we let $T_i$ denote the term that course $i$ is assigned to, i.e., $T_i = j \iff x_{ij} = 1$, then the second condition, which requires the assignment to satisfy all requisites, yeilds three constraints depending upon the requisite type. That is, if course $a$ is a *requisite* for course $b$, then: +If we let $T_i$ denote the term that course $i$ is assigned to, i.e., $T_i = j \iff x_{ij} = 1$, then the second condition, which requires the assignment to satisfy all requisites, yields three constraints depending upon the requisite type. That is, if course $a$ is a *requisite* for course $b$, then: ```math \mbox{Constraint 2 (prerequisite):} \ \ T_a \ < \ T_b, \\ @@ -51,7 +51,7 @@ Note that $T_i$ can be obtained from the assignment matrix using: T_i = \sum_{j=1}^m j \cdot x_{ij}. ``` -In order to guide the optimzation algorithms towards reasonable soluations, additional constraints are required. In partciular, it is necessarey to specify the maximum number of terms you would like the degree plan to contain, denoted $\alpha$, as well as the minimum and maximum number of credit hours allowed in each term, denoted $\beta$ and $\gamma$ respectively. If we let $c_i$ denote the number of credit hours associated with course $i$, and $\theta_j$ the number of credit hours in term $j$, then +In order to guide the optimization algorithms towards reasonable solutions, additional constraints are required. In particular, it is necessarFurthemore, this toolbox supports a multi-objectivey to specify the maximum number of terms you would like the degree plan to contain, denoted $\alpha$, as well as the minimum and maximum number of credit hours allowed in each term, denoted $\beta$ and $\gamma$ respectively. If we let $c_i$ denote the number of credit hours associated with course $i$, and $\theta_j$ the number of credit hours in term $j$, then ```math \theta_j = \sum_{i=1}^n c_i \cdot x_{ij}, \ \ \ \ j = 1, \ldots, m, @@ -67,7 +67,7 @@ In order to guide the optimzation algorithms towards reasonable soluations, addi ### Objective Functions -A number of different objective functions have been defined for use in creating degree plans optimized around particular criteria. Furthemore, this toolbox supports a multi-objetive framework, allowing more than one of these objective functions to be simultaneously applied while creating degree plans. +A number of different objective functions have been defined for use in creating degree plans optimized around particular criteria. Furthermore, this toolbox supports a multi-objective framework, allowing more than one of these objective functions to be simultaneously applied while creating degree plans. For a single objective function $f(x)$, the optimzation problem can be stated as: ```math @@ -75,7 +75,7 @@ For a single objective function $f(x)$, the optimzation problem can be stated as \mbox{subject to: Constraints} \ \ 1-7. ``` -For multiple objective functions $f_1(x), f(_2(x), \ldots$ the mulit-objective optimzation problem can be stated as: +For multiple objective functions $f_1(x), f(_2(x), \ldots$ the multi-objective optimization problem can be stated as: ```math \min \left\{ f_1(x), \ f_2(x), \ldots \right\}, \\ \mbox{subject to: Constraints} \ \ 1-7. @@ -107,16 +107,16 @@ Let $-1 \leq \aleph_{ij} \leq 1$ denote the toxic impact that course $i$ has on f(x) = \min \left( \sum_{t=1}^m \sum_{i=1}^n \sum_{j=1}^n \aleph_{ij} \cdot x_{it} \cdot x_{jt} \right). ``` -The `optimize_plan` function in the toolbox implements the optimziation problems described above. +The `optimize_plan` function in the toolbox implements the optimization problems described above. ```julia optimize_plan(c::Curriculum, term_count::Int, min_cpt::Int, max_cpt::Int, obj_order::Array{String, 1}; diff_max_cpt::Array{UInt, 1}, fix_courses::Dict, consec_courses::Dict, term_range::Dict, prior_courses::Array{Term, 1}) ``` -Using the curriculum `c` supplied as input, returns a degree plan optimzed according to the various +Using the curriculum `c` supplied as input, returns a degree plan optimized according to the various optimization criteria that have been specified as well as the objective functions that have been selected. -If an optimzied plan cannot be constructed (i.e., the constraints are such that an optimal solution is infeasible), +If an optimized plan cannot be constructed (i.e., the constraints are such that an optimal solution is infeasible), `nothing` is returned, and the solver returns a message indicating that the problems is infeasible. In these cases, you may wish to experiment with the constraint values. diff --git a/src/CurricularAnalytics.jl b/src/CurricularAnalytics.jl index 20f4daef..0ba175cf 100644 --- a/src/CurricularAnalytics.jl +++ b/src/CurricularAnalytics.jl @@ -30,7 +30,7 @@ export Degree, AA, AS, AAS, BA, BS, System, semester, quarter, Requisite, pre, c reachable_from_subgraph, reachable_to, reachable_to_subgraph, reach, reach_subgraph, isvalid_curriculum, extraneous_requisites, blocking_factor, delay_factor, centrality, complexity, dead_ends, courses_from_vertices, compare_curricula, similarity, homology, isvalid_degree_plan, print_plan, visualize, metric_histogram, metric_boxplot, - show_homology, basic_metrics, basic_statistics, read_csv, create_degree_plan, bin_packing, bin_packing2, find_min_terms, + show_homology, basic_metrics, basic_statistics, read_csv, create_degree_plan, bin_filling, find_min_terms, add_lo_requisite!, update_plan, write_csv, find_min_terms, balance_terms, requisite_distance, balance_terms_opt, find_min_terms_opt, read_Opt_Config, optimize_plan, json_to_julia, julia_to_json, init_opt @@ -371,7 +371,7 @@ function complexity(c::Curriculum) return c.metrics["complexity"] = curric_complexity, course_complexity end -# Find all fo the longest paths in a curriculum. +# Find all the longest paths in a curriculum. """ longest_paths(c::Curriculum) diff --git a/src/DataTypes.jl b/src/DataTypes.jl index 2625ff08..7876b382 100644 --- a/src/DataTypes.jl +++ b/src/DataTypes.jl @@ -126,7 +126,7 @@ mutable struct Course this.metrics = Dict{String, Any}() this.metadata = Dict{String, Any}() this.learning_outcomes = learning_outcomes - this.vertex_id = Dict{Int, Int}() + this.vertex_id = Dict{Int, Int}() # curriculum id -> vertex id return this end end diff --git a/src/DegreePlanAnalytics.jl b/src/DegreePlanAnalytics.jl index acb1d5ab..0bdfdb79 100644 --- a/src/DegreePlanAnalytics.jl +++ b/src/DegreePlanAnalytics.jl @@ -76,28 +76,28 @@ function basic_metrics(plan::DegreePlan) # Degree plan metrics based upon the distance between requsites and the classes that require them. """ - requisite_distance(DegreePlan, course::Int) + requisite_distance(DegreePlan, course::Course) -For a given degree plan `plan` and course `course`, this function computes the total distance between `course` and -each of its requisites. +For a given degree plan `plan` and target course `course`, this function computes the total distance between `course` and +all of its requisites. # Arguments Required: - `plan::DegreePlan` : a valid degree plan (see [Degree Plans](@ref)). -- `course::Int` : the vertex ID of a course in the curriclum graph `plan.curriculum.graph`. +- `course::Course` : the target course. - -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 we let ``T_i^p`` denote the term in degree plan ``p`` that course ``c_i`` -appears in, then for a degree plan with underlying curriculum graph ``G_c = (V,E)``, the requisite distance for course -``c_i`` in degree plan ``p``, denoted ``rd_{v_i}^p``, is: +The distance between a target course and one of its requisites is given by the number of terms that separate the target +course from the particular requisite in the degree plan. To compute the requisite distance, we sum this distance over all +requisites. That is, if let ``T_i^p`` denote the term in degree plan ``p`` that course ``c_i`` appears in, then for a +degree plan with underlying curriculum graph ``G_c = (V,E)``, the requisite distance for course ``c_i`` in degree plan ``p``, +denoted ``rd_{v_i}^p``, is: ```math rd_{v_i}^p = \\sum{(v_i, v_j) \\in E} (T_i - T_j). ``` 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 will be stored in the associated `Course` data object. +The requisite distance metric computed by this function is stored in the associated `Course` data object. """ function requisite_distance(plan::DegreePlan, course::Course) distance = 0 diff --git a/src/DegreePlanCreation.jl b/src/DegreePlanCreation.jl index 8e888816..e11b28c4 100644 --- a/src/DegreePlanCreation.jl +++ b/src/DegreePlanCreation.jl @@ -1,7 +1,8 @@ # file: DegreePlanCreation.jl -function create_degree_plan(curric::Curriculum, create_terms::Function=bin_packing, name::AbstractString="", additional_courses::Array{Course}=Array{Course,1}(); + +function create_degree_plan(curric::Curriculum, create_terms::Function=bin_filling, name::AbstractString="", additional_courses::Array{Course}=Array{Course,1}(); min_terms::Int=1, max_terms::Int=8, min_credits_per_term::Int=3, max_credits_per_term::Int=19) - terms = create_terms(curric, additional_courses; min_terms=min_terms, max_terms=max_terms, min_credits_per_term=min_credits_per_term, + terms = create_terms(curric, additional_courses; min_terms=min_terms, max_terms=max_terms, min_credits_per_term=min_credits_per_term, max_credits_per_term=max_credits_per_term) if terms == false println("Unable to create degree plan") @@ -11,214 +12,63 @@ function create_degree_plan(curric::Curriculum, create_terms::Function=bin_packi end end -function check_requistes(curric::Curriculum, index::Int, previous_terms::Array{Int}, current_term::Array{Int}) - req_complete = true - # find all inneighbors of vertex with index supplied as input - inngbr = inneighbors(curric.graph, index) - for ngbr in inngbr - req_type = curric.courses[index].requisites[curric.courses[ngbr].id] - if req_type == pre && !(ngbr in previous_terms) - req_complete = false - break - elseif req_type == co && !(ngbr in previous_terms) && !(ngbr in current_term) - req_complete = false - break - end - end - return req_complete -end - -function bin_packing(curric::Curriculum, additional_courses::Array{Course}=Array{Course,1}(); - min_terms::Int=1, max_terms::Int=1, min_credits_per_term::Int=5, max_credits_per_term::Int=19) - curric_total_credit=total_credits(curric) - if !("complexity" in keys(curric.metrics)) - complexity(curric) - end - sorted_index = sortperm(curric.metrics["complexity"][2], rev=true) # returns a permutation vector - terms = Array{Term}(undef, min_terms) - all_applied_courses = Int[] - for current_term in 1:min_terms - termclasses = Course[] - this_term_applied_courses = Int[] - total_credits_for_current_term = 0 - # Find upper limit of average credit for remaining terms to balance the credit hours of terms - avrg_credit_remaining = floor(Int, (curric_total_credit + min_terms - current_term)/ (min_terms-current_term + 1)) - # Check if upper limit of average credit hours for remaining terms exceeds the maximum credits per term. - # If it does, there is no way to fit remaining classses in, so try again after increasing the number of terms. - if avrg_credit_remaining < max_credits_per_term - # Go through all courses to add in current term according to the complexity score - for index in sorted_index - # Ignore if current course was already added to a previous term - if !(index in all_applied_courses) && !(index in this_term_applied_courses) - # Make sure requisites are satisfied - can_be_added = true - if check_requistes(curric, index, all_applied_courses, this_term_applied_courses) - credit_add = curric.courses[index].credit_hours - courses_to_add = [index] - outnbr = outneighbors(curric.graph, index) - for ngbr in outnbr - req_type = curric.courses[ngbr].requisites[curric.courses[index].id] - if req_type == strict_co - can_be_added = check_requistes(curric, ngbr, all_applied_courses, this_term_applied_courses) - if !can_be_added - break - end - credit_add += curric.courses[ngbr].credit_hours - push!(courses_to_add, ngbr) - end - end - # Add current course if it does not overflow the bin - if can_be_added && total_credits_for_current_term + credit_add <= avrg_credit_remaining - total_credits_for_current_term += credit_add - for course_index in courses_to_add - push!(termclasses, curric.courses[course_index]) - # Track indicies of current term's courses - push!(this_term_applied_courses, course_index) - end - end - # Any credit remaining? - if total_credits_for_current_term == avrg_credit_remaining - break - end - end - end - end - # Substract credits of added courses - curric_total_credit = curric_total_credit - total_credits_for_current_term - # Create term - terms[current_term] = Term(termclasses) - # Before starting a new term, add all indices of the current term's classes - for course_in_term in this_term_applied_courses - push!(all_applied_courses, course_in_term) +function bin_filling(curric::Curriculum, additional_courses::Array{Course}=Array{Course,1}(); + min_terms::Int=1, max_terms::Int=8, min_credits_per_term::Int=3, max_credits_per_term::Int=19) + terms = Array{Term,1}() + term_credits = 0 + term_courses = Course[] + UC = sort!(deepcopy(curric.courses), by=course_num) # 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)) + if term_credits + c.credit_hours <= max_credits_per_term + append!(term_courses, [c]) + term_credits = term_credits + c.credit_hours + else + append!(terms, [Term(term_courses)]) + term_courses = Course[c] + term_credits = c.credit_hours end + else # can't find a course to add to current term, create a new term + length(term_courses) > 0 ? append!(terms, [Term(term_courses)]) : nothing + term_courses = Course[] + term_credits = 0 end end - if length(all_applied_courses) != length(sorted_index) - if min_terms < max_terms - # The following print statement can be uncommented for debugging purposes - # println("Unable to create a $min_terms term plan, attempting a $(min_terms+1) term plan") - return bin_packing(curric, additional_courses; min_terms=min_terms+1, max_terms=max_terms, min_credits_per_term=min_credits_per_term, - max_credits_per_term=max_credits_per_term) - else - return false # No solution, the degree plans requires more terms than max_terms - end - end + length(term_courses) > 0 ? append!(terms, [Term(term_courses)]) : nothing return terms end -function create_terms(curric::Curriculum; term_count::Int, min_credits_per_term::Int=5, max_credits_per_term::Int=19) - if !("complexity" in keys(curric.metrics)) - complexity(curric) - end - sorted_index = sortperm(curric.metrics["complexity"][2], rev=true) - terms = Array{Term}(undef, term_count) - curric_total_credit=total_credits(curric) - added_credits = 0 - all_applied_courses = Int[] - for current_term in 1:term_count - termclasses = Course[] - this_term_applied_courses = Int[] - total_credits_for_current_term = 0 - # Can remaining credits be added to the remaining terms? - if (curric_total_credit - added_credits) <= ((term_count - current_term+1) * max_credits_per_term) - for index in sorted_index - if !(index in all_applied_courses) && !(index in this_term_applied_courses) - can_be_added = true - if check_requistes(curric, index, all_applied_courses, this_term_applied_courses) - credit_add = curric.courses[index].credit_hours - courses_to_add = [index] - for ngbr in outneighbors(curric.graph, index) - req_type = curric.courses[ngbr].requisites[curric.courses[index].id] - if req_type == strict_co - can_be_added = check_requistes(curric, ngbr, all_applied_courses, this_term_applied_courses) - if !can_be_added - break - end - credit_add += curric.courses[ngbr].credit_hours - push!(courses_to_add,ngbr) - end - end - if can_be_added && total_credits_for_current_term + credit_add <= max_credits_per_term - total_credits_for_current_term += credit_add - for course_index in courses_to_add - push!(termclasses, curric.courses[course_index]) - push!(this_term_applied_courses,course_index) - end - end - #Check if current term is full - if total_credits_for_current_term == max_credits_per_term - #There is no more space for any other course - break - end - end - end - end - added_credits += total_credits_for_current_term - terms[current_term] = Term(termclasses) - for course in this_term_applied_courses - push!(all_applied_courses, course) +function select_vertex(curric::Curriculum, term_courses::Array{Course,1}, UC::Array{Course,1}) + for target in UC + t_id = target.vertex_id[curric.id] + UCs = deepcopy(UC) + deleteat!(UCs, findfirst(c->c.id==target.id, UCs)) + invariant1 = true + for source in UCs + s_id = source.vertex_id[curric.id] + vlist = reachable_from(curric.graph, s_id) + if t_id in vlist # target cannot be moved to AC + invariant1 = false # invariant 1 violated + break # try a new target end - else - return nothing, false - end - end - if length(all_applied_courses) == length(sorted_index) - return terms, true - else - return nothing, false - end -end - -#""" -#find_min_terms function will find the minimum number terms possible to fit all courses with the respect that all requisite -# conditions and returns a tuple which contains 3 elements. -# 1- Boolean value which shows if term list created for term count. -# 2- Term list which contains all courses for related term id. -# 3- The term_count is a integer value to show minimum number of terms possible to fit all courses. -# * Altough this function returns term list, it does not guarantee that each term will have the same number of credit hours. -# It will put all courses as early term as possible according to the complexity score. -#""" -function find_min_terms(curric::Curriculum, additional_courses::Array{Course}=Array{Course,1}(); - min_terms::Int=1, max_terms::Int=10, min_credits_per_term::Int=5, max_credits_per_term::Int=19) - for term_count in range(min_terms, stop=max_terms) - terms, control = create_terms(curric; term_count=term_count, min_credits_per_term = min_credits_per_term, - max_credits_per_term = max_credits_per_term) - if control - return true, terms, term_count end - end - return false, nothing, nothing -end - -#""" -#balance_terms function will spread all courses to the provided number of terms with minimum number of difference between terms. -# In other words, balance terms according to the number of credit hours and returns a tuple which contains 3 elements. -# 1- Boolean value which shows if term list created for term count. -# 2- Term list which contains all courses for related term id. -# 3- The max_credit is a integer value to show maximum number of credit hours assigned to any of the list of terms. -#""" -function balance_terms(curric::Curriculum, additional_courses::Array{Course}=Array{Course,1}(); - term_count::Int=1, min_credits_per_term::Int=1, max_credits_per_term::Int=19) - for max_credit in range(min_credits_per_term, length=max_credits_per_term-min_credits_per_term+1) - terms, control = create_terms(curric; term_count=term_count, min_credits_per_term = min_credits_per_term, - max_credits_per_term = max_credit) - if control - return true, terms, max_credit + 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 + invariant2 = false + break # try a new target + end + end + if invariant2 == true + return target + end end end - return false, nothing, nothing + return nothing end -function bin_packing2(curric::Curriculum, additional_courses::Array{Course}=Array{Course,1}(); - min_terms::Int=1, max_terms::Int=8, min_credits_per_term::Int=3, max_credits_per_term::Int=19) - control, terms, min_term_count = find_min_terms(curric, additional_courses; min_terms = min_terms,max_terms = max_terms, min_credits_per_term = min_credits_per_term, max_credits_per_term = max_credits_per_term) - if control - control_balance, terms, max_credit = balance_terms(curric, additional_courses; - term_count=min_term_count, min_credits_per_term = min_credits_per_term, max_credits_per_term=max_credits_per_term) - if control_balance - return terms - end - end - return false +function course_num(c::Course) + c.num != "" ? c.num : c.name end - diff --git a/src/GraphAlgs.jl b/src/GraphAlgs.jl index a2438956..bf73a03b 100644 --- a/src/GraphAlgs.jl +++ b/src/GraphAlgs.jl @@ -314,7 +314,7 @@ function longest_path(g::AbstractGraph{T}, s::Int) where T return lp end -# Find all fo the longest paths in an acyclic graph. +# Find all of the longest paths in an acyclic graph. """ longest_paths(g) diff --git a/src/Visualization.jl b/src/Visualization.jl index 19c57636..a411ccec 100644 --- a/src/Visualization.jl +++ b/src/Visualization.jl @@ -77,14 +77,14 @@ Keyword: """ function visualize(curric::Curriculum; changed=nothing, notebook::Bool=false, edit::Bool=false, min_term::Int=1, output_file="edited_curriculum.csv", - show_delay::Bool=false, show_blocking::Bool=false, show_centrality::Bool=false, show_complexity::Bool=false, scale::Real=1) + show_delay::Bool=true, show_blocking::Bool=true, show_centrality::Bool=true, show_complexity::Bool=true, scale::Real=1) num_courses = curric.num_courses if num_courses <= 8 - max_credits_per_term = 12 + max_credits_per_term = 9 elseif num_courses <= 16 - max_credits_per_term = 15 + max_credits_per_term = 12 elseif num_courses <= 24 - max_credits_per_term = 18 + max_credits_per_term = 15 elseif num_courses <= 32 max_credits_per_term = 18 elseif num_courses <= 40 @@ -107,9 +107,9 @@ function visualize(curric::Curriculum; changed=nothing, notebook::Bool=false, ed error("Curriculum is too big to visualize.") end term_count = num_courses - dp = create_degree_plan(curric, bin_packing2; max_terms = term_count, max_credits_per_term = max_credits_per_term) + dp = create_degree_plan(curric, bin_filling, max_credits_per_term = max_credits_per_term) viz_helper(dp; changed=changed, notebook=notebook, edit=edit, hide_header=true, output_file=output_file, show_delay=show_delay, - show_blocking=show_blocking,show_centrality=show_centrality, show_complexity=show_complexity, scale=scale) + show_blocking=show_blocking, show_centrality=show_centrality, show_complexity=show_complexity, scale=scale) end # Main visualization function. A "changed" callback function may be provided which will be invoked whenever the diff --git a/test/DegreePlanCreation.jl b/test/DegreePlanCreation.jl new file mode 100644 index 00000000..762da8fd --- /dev/null +++ b/test/DegreePlanCreation.jl @@ -0,0 +1,31 @@ +# DegreePlanCreation tests + +@testset "DegreePlanCreation Tests" begin +# +# 4-course curriculum - only one minimal degree plan +# +# A --------* B +# +# +# C --------* D +# +# (A,B) - pre; (C,D) - pre + +A = Course("A", 3, institution="ACME State", prefix="BW", num="101", canonical_name="Baskets I") +B = Course("B", 3, institution="ACME State", prefix="BW", num="201", canonical_name="Baskets II") +C = Course("C", 3, institution="ACME State", prefix="BW", num="102", canonical_name="Basket Apps I") +D = Course("D", 3, institution="ACME State", prefix="BW", num="202", canonical_name="Basket Apps II") + +add_requisite!(A,B,pre) +add_requisite!(C,D,pre) + +curric = Curriculum("Basket Weaving", [A,B,C,D], institution="ACME State") + +terms = bin_filling(curric, max_credits_per_term=6) + +@test terms[1].courses[1].name == "A" || terms[1].courses[1].name == "C" +@test terms[1].courses[2].name == "A" || terms[1].courses[2].name == "C" +@test terms[2].courses[1].name == "B" || terms[2].courses[1].name == "D" +@test terms[2].courses[2].name == "B" || terms[2].courses[2].name == "D" + +end; diff --git a/test/runtests.jl b/test/runtests.jl index f2aad51c..54b424d0 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -11,4 +11,5 @@ include("DataTypes.jl") include("CurricularAnalytics.jl") include("GraphAlgs.jl") include("DegreePlanAnalytics.jl") -include("DataHandler.jl") \ No newline at end of file +include("DataHandler.jl") +include("DegreePlanCreation.jl") \ No newline at end of file