diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fd7125..d4dcaa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,68 @@ All notable changes to NuAnalytics are recorded here. Format loosely follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the project uses semantic versioning. +## [0.5.0] — 2026-06-09 + +Adds database-backed degree storage: a normalized, queryable projection of +imported degree programs alongside the lossless source document, plus the CLI, +MCP, and analyze paths that write to and read from it. Additive — no breaking +changes. + +### Added + +- **Normalized program storage schema** (`docs/database/programs-schema.sql`, + seeded by `docs/database/program-lookup-seed.sql`). Eight new tables: + `degree_types` (lookup), `programs` (one row per program, with the lossless + unified-JSON `document` JSONB as the source of truth plus a queryable scalar + projection), `courses` (shared per-institution catalog), `program_courses` + (M:N junction with per-program credit/name overrides), `program_requirements` + (the requirement tree flattened by `req_path`/`parent_path`, with JSONB + `selection_spec`/`req_constraints` and `is_impossible`/`allow_double_count` + flags), `analysis_runs` (one per `degree analyze` run: `variant`, `trimmed`, + `variations_run`, `sample_type`, the `degree_metrics` JSONB plus promoted + `complexity_mean`/`delay_mean`/`credits_mean`), `analysis_course_metrics` + (per run × course), and `analysis_plans` (per run × selected plan). Design is + hybrid (lossless document + normalized projection) with FK-free natural keys + (`program_key`, `(institution_ref, course_code)`) matching the existing + `degrees`/`completions` convention, RLS gated on + `auth.role() = 'authenticated'` for every read and write, and idempotent + re-sync via a per-import `generation` stamp. Apply with + `nuanalytics db exec-sql docs/database/programs-schema.sql` then + `nuanalytics db exec-sql docs/database/program-lookup-seed.sql`. + +- **`db import ...`** — import degree-first analysis reports + (`*_report.json`) or plain unified/ai-landscape/YAML degrees into the + normalized program tables. One report populates the program projection + (`programs`, `courses`, `program_courses`, `program_requirements`) and, when + it carries an `analysis` block, one analysis run with its course metrics and + selected plans. Resolves the institution (`degree.unitid` fast-path → name+CIP + lookup → name slug; an ambiguous name lists candidate institutions and writes + nothing). `program_key` includes `catalog_year`, so different years coexist; + overwriting an existing program needs `--replace` (unverified) or `--force` + (verified). Flags: `--variant`, `--unitid`, `--institution`, `--cip`, + `--catalog`, `--degree-id`, `--force`, `--replace`, `--skip-existing`, + `--dry-run`, `-j/--jobs`. A single file prints a detailed outcome; a + directory/batch isolates per-file failures to `import_failures.log` with a + summary. + +- **`import_degree` MCP tool** — the same import core over MCP. Takes + `json_content` or `json_path` (exactly one) plus the import overrides; returns + a structured result tag (`created`/`updated`/`skipped`/`needs_confirmation`/ + `institution_ambiguous`/`rejected`) with the row counts, and attaches + `institution_candidates`/`reason`/`errors` on the blocked variants. DB-gated + (registered only with a logged-in session); `dry_run` previews without + writing. + +- **`degree analyze --from-db `** — analyze a stored program pulled from + the database instead of a file. Resolves by exact `program_key`, then exact + `degree_id`, then a `name` substring; an ambiguous name lists the candidates + and stops. Mutually exclusive with positional `FILES`; single-program only + (no worker pool). + +- **`degree.unitid` field** — optional IPEDS unit id on the degree model, set in + a degree file's `degree:` block, used to link an imported program to its + institution. + ## [0.4.1] — 2026-06-04 This release is additive — no breaking changes from 0.4.0. It introduces a diff --git a/Cargo.lock b/Cargo.lock index 7f131ec..8fa367e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1369,7 +1369,7 @@ dependencies = [ [[package]] name = "nu-analytics" -version = "0.4.1" +version = "0.5.0" dependencies = [ "chrono", "clap", @@ -1383,6 +1383,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "sha2", "supabase-client-sdk", "tempfile", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 3dab1c4..7a1f766 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nu-analytics" -version = "0.4.1" +version = "0.5.0" edition = "2021" authors = ["Albert Lionelle "] description = "A CLI tool for computing Curricular Analytics metrics. Based off metrics from CurricularAnalytics.org. Currently, only basic metrics and additional reporting features." @@ -40,6 +40,7 @@ reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-feature csv = { version = "1.3", optional = true } zip = { version = "8", optional = true } form_urlencoded = { version = "1.2", optional = true } +sha2 = { version = "0.10", optional = true } [dev-dependencies] tempfile = "3.10" @@ -57,7 +58,7 @@ log-info = [] log-debug = [] verbose = [] file-logging = [] -database = ["dep:supabase-client-sdk", "dep:reqwest", "dep:csv", "dep:zip", "dep:tokio", "dep:form_urlencoded"] +database = ["dep:supabase-client-sdk", "dep:reqwest", "dep:csv", "dep:zip", "dep:tokio", "dep:form_urlencoded", "dep:sha2"] mcp = ["dep:rmcp", "dep:tokio", "database"] [profile.release] diff --git a/Readme.md b/Readme.md index 052cdbb..3bbd960 100644 --- a/Readme.md +++ b/Readme.md @@ -1,6 +1,6 @@ # NuAnalytics -**Version 0.4.1** +**Version 0.5.0** NuAnalytics is a Rust-based tool for analyzing computer science curricula. It computes detailed metrics about curriculum structure including complexity, blocking relationships, delay paths, and centrality measures to help understand how courses are organized and their impact on students. @@ -35,6 +35,7 @@ It is based off the work of Greg Heileman, and CurricularAnalytics.org. Current - **Configuration Management**: Flexible configuration system with CLI overrides - **MCP Server** (optional): AI model integration for interactive degree building via Model Context Protocol. Degree tools accept YAML, unified JSON, or raw ai-landscape JSON; `convert_degree` *(new in 0.4.1)* converts ai-landscape JSON to the unified format and `get_degree_json_schema` *(new in 0.4.1)* returns the machine schema. The `trim_degree` tool pipes a fresh `cache:` handle back for chained `validate_degree` / `audit_degree` calls. - **Authenticated Database Access** *(behavior change in 0.4.0)*: Supabase reads and writes both require a logged-in user (`nuanalytics db login`); session tokens auto-refresh. +- **Degree Database Import & Stored Programs** *(new in 0.5.0)*: Import degree reports into a normalized, queryable program store (`nuanalytics db import`, or the `import_degree` MCP tool) — one report becomes a program plus its courses, flattened requirement tree, and analysis run, alongside the lossless source document. Stored programs can be analyzed straight from the database with `degree analyze --from-db `. See [Database Setup](docs/database/setup.md#stored-programs-normalized). ## Quick Start diff --git a/docs/database/program-lookup-seed.sql b/docs/database/program-lookup-seed.sql new file mode 100644 index 0000000..fb512e2 --- /dev/null +++ b/docs/database/program-lookup-seed.sql @@ -0,0 +1,21 @@ +-- NuAnalytics Program Lookup Seed +-- Run after docs/database/programs-schema.sql, via: +-- nuanalytics db exec-sql docs/database/program-lookup-seed.sql +-- +-- Seeds the degree_types lookup. Codes are the normalized degree_type values the +-- importer writes to programs.degree_type. Values observed in the corpus +-- (json_corrected + json_corrected_rebuilt): BS, BA, MS, Minor, Certificate, +-- BSE, BAS, AS, Micro-Credential. Idempotent (ON CONFLICT DO UPDATE). + +INSERT INTO degree_types (code, label, level, is_degree) VALUES + ('BS', 'Bachelor of Science', 'undergraduate', true), + ('BA', 'Bachelor of Arts', 'undergraduate', true), + ('BSE', 'Bachelor of Science in Engineering', 'undergraduate', true), + ('BAS', 'Bachelor of Applied Science', 'undergraduate', true), + ('AS', 'Associate of Science', 'undergraduate', true), + ('MS', 'Master of Science', 'graduate', true), + ('MINOR', 'Minor', 'undergraduate', false), + ('CERT', 'Certificate', NULL, false), + ('MICRO', 'Micro-Credential', NULL, false) +ON CONFLICT (code) DO UPDATE + SET label = EXCLUDED.label, level = EXCLUDED.level, is_degree = EXCLUDED.is_degree; diff --git a/docs/database/programs-schema.sql b/docs/database/programs-schema.sql new file mode 100644 index 0000000..57486f2 --- /dev/null +++ b/docs/database/programs-schema.sql @@ -0,0 +1,364 @@ +-- NuAnalytics Programs Schema (normalized degree storage) +-- Run in Supabase SQL Editor or via: nuanalytics db exec-sql docs/database/programs-schema.sql +-- +-- After running this file, also run: +-- docs/database/program-lookup-seed.sql (degree_types lookup) +-- +-- Adds the normalized, queryable storage for imported degree programs: +-- programs one row per degree program (+ lossless `document` JSONB) +-- courses shared per-institution course catalog +-- program_courses M:N junction programs <-> courses (with per-program overrides) +-- program_requirements the requirement tree, flattened by `req_path` +-- degree_types small lookup for degree_type codes +-- +-- Design notes: +-- * Rows link by NATURAL keys (program_key, (institution_ref, course_code)), +-- NOT surrogate-id FKs — matching the FK-free / LEFT JOIN convention used by +-- `degrees` and `completions`, so write order never matters. +-- * `programs.document` is the authoritative, lossless unified-JSON degree; +-- the normalized rows are a queryable projection. Reads reconstruct from +-- `document`, so a partially-written projection is never user-visible. +-- * Child rows carry a `generation` stamp; a re-import bumps the program's +-- `generation` last (the commit marker) and stale children +-- (generation < programs.generation) are filtered at read time / GC'd later. +-- +-- Safe to run on a live database: every object uses IF NOT EXISTS and policies +-- are dropped before being (re)created, so the file is idempotent. + +-- Trigram index support for course-name substring search ("all calculus courses"). +CREATE EXTENSION IF NOT EXISTS pg_trgm; + +-- ============================================================================= +-- degree_types — lookup for normalized degree_type codes +-- Seeded from docs/database/program-lookup-seed.sql. +-- ============================================================================= +CREATE TABLE IF NOT EXISTS degree_types ( + code TEXT PRIMARY KEY, -- 'BS','BA','BSE','BAS','AS','MS','MINOR','CERT','MICRO' + label TEXT NOT NULL, + level TEXT, -- 'undergraduate' | 'graduate' + is_degree BOOLEAN NOT NULL DEFAULT true +); + +-- ============================================================================= +-- programs — one row per degree program +-- +-- program_key is the deterministic idempotency key (ON CONFLICT target): +-- "prog:|||" (when the unitid resolved) +-- "prog:|" (else, when degree.id is present) +-- "fp:" (fallback) +-- +-- No FK on unitid / cip_code / degree_type: LEFT JOIN institutions / cip_codes / +-- degree_types (IPEDS cross-survey coverage isn't guaranteed; degree_type may be +-- a value not yet seeded). +-- ============================================================================= +CREATE TABLE IF NOT EXISTS programs ( + id BIGSERIAL PRIMARY KEY, + program_key TEXT UNIQUE NOT NULL, + + -- identity / provenance + degree_id TEXT, -- Degree.id (may be null; non-unique here) + unitid INTEGER, -- resolved IPEDS unit id (no FK) + institution_ref TEXT NOT NULL, -- unitid-as-text when resolved, else normalized slug + institution_raw TEXT, -- original Degree.institution string + cip_code TEXT, -- no FK; LEFT JOIN cip_codes + + -- queryable type dimensions + name TEXT NOT NULL, + degree_type TEXT, -- normalized -> degree_types.code (no FK) + program_kind TEXT, -- major|minor|concentration|certificate|track|specialization|emphasis|micro + discipline TEXT, -- ai|cs|ds|cy|... + system_type TEXT NOT NULL DEFAULT 'semester', + tags TEXT[], -- raw tag set (GIN-indexed) + + -- scalar projection of Degree + catalog_year TEXT, + source_url TEXT, + total_credits INTEGER, + upper_division_credits INTEGER, + in_major_credits INTEGER, + gpa_minimum REAL, + gpa_major REAL, + grade_minimum TEXT, + major_subjects TEXT[], + allow_double_counting BOOLEAN, -- GLOBAL DEFAULT ONLY; effective control is per-requirement + + -- authoritative document + control + document JSONB NOT NULL, -- lossless unified-JSON degree (source of truth) + document_hash TEXT NOT NULL, -- sha256 of canonical document (change detection) + verified BOOLEAN NOT NULL DEFAULT false, -- human-confirmed; gates overwrite + institution_resolved BOOLEAN NOT NULL DEFAULT false, + has_impossible_requirements BOOLEAN NOT NULL DEFAULT false, + generation BIGINT NOT NULL DEFAULT 0, -- re-import commit marker + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================================= +-- courses — shared, per-institution course catalog +-- +-- institution_ref partitions the catalog (so courses dedup within an institution +-- even when its unitid is unresolved). Canonical attributes are best-effort +-- (last-write-wins); a program's divergent credits/name live on program_courses. +-- Courses are NOT generation-swept per program (they belong to many programs). +-- ============================================================================= +CREATE TABLE IF NOT EXISTS courses ( + id BIGSERIAL PRIMARY KEY, + institution_ref TEXT NOT NULL, + unitid INTEGER, + course_code TEXT NOT NULL, -- the DegreeProgram course-map key, e.g. "CMPSC121" + prefix TEXT, + number TEXT, + name TEXT, + credit_hours REAL, + credit_min INTEGER, + credit_max INTEGER, + prerequisites JSONB, -- {and|or|leaf} tree + prerequisites_raw TEXT, + gen_ed_attributes TEXT[], + cross_listed_as TEXT[], + tags TEXT[], + generation BIGINT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE (institution_ref, course_code) +); + +-- ============================================================================= +-- program_courses — M:N junction programs <-> courses +-- Joins to courses on (institution_ref, course_code). Carries per-program +-- overrides for the divergent-attribute case. Generation-swept per program. +-- ============================================================================= +CREATE TABLE IF NOT EXISTS program_courses ( + id BIGSERIAL PRIMARY KEY, + program_key TEXT NOT NULL, + institution_ref TEXT NOT NULL, + course_code TEXT NOT NULL, + credit_hours_override REAL, + name_as_listed TEXT, + generation BIGINT NOT NULL DEFAULT 0, + UNIQUE (program_key, course_code) +); + +-- ============================================================================= +-- program_requirements — the requirement tree, flattened +-- +-- req_path is a deterministic address of the node within the requirement tree; +-- parent_path gives the one_of adjacency. e.g. a one_of `concentrations`, option +-- `thesis`, its 2nd nested requirement -> req_path='concentrations#thesis#1', +-- parent_path='concentrations', option_id='thesis'. Top-level map entry `core` +-- -> req_path='core', parent_path=NULL, map_key='core'. +-- +-- The irreducible/recursive bits stay as JSONB: `courses` (type=all list), +-- `selection_spec` (the whole from-clause incl. patterns/groups/include/exclude; +-- wildcards like "CS:2500+"/"*:*" aren't enumerable), and `req_constraints`. +-- Generation-swept per program. +-- ============================================================================= +CREATE TABLE IF NOT EXISTS program_requirements ( + id BIGSERIAL PRIMARY KEY, + program_key TEXT NOT NULL, + req_path TEXT NOT NULL, + parent_path TEXT, -- NULL for top-level; else parent req_path + map_key TEXT, -- top-level requirements-map key (NULL for nested) + option_id TEXT, -- RequirementOption.id when under a one_of option + option_name TEXT, + name TEXT, + req_type TEXT NOT NULL, -- 'all' | 'select' | 'one_of' + category TEXT, -- major | supporting | gen_ed | elective + count INTEGER, + credits INTEGER, + credit_min INTEGER, + credit_max INTEGER, + tags TEXT[], + courses JSONB, -- Vec for type=all + selection_spec JSONB, -- the FromClause (courses/pattern/include/exclude/groups/...) + req_constraints JSONB, -- RequirementConstraints + is_impossible BOOLEAN NOT NULL DEFAULT false, -- count > resolvable pool (queryable, not dropped) + allow_double_count BOOLEAN, -- derived from constraints.exclude_used (+ program default) + generation BIGINT NOT NULL DEFAULT 0, + UNIQUE (program_key, req_path) +); + +-- ============================================================================= +-- analysis_runs — one row per `degree analyze` run of a program +-- +-- Metrics are NOT columns on programs because they vary by run parameters +-- (iterations / sampling / mean-vs-median) AND by whether the analyzed degree +-- was trimmed. A run links to its parent program (program_key) but records the +-- exact analyzed artifact: `analyzed_document_hash` (always) and +-- `analyzed_document` (only when it differs from the program's document, e.g. a +-- trimmed variant that is NOT itself a stored program). Different configurations +-- of the same program coexist as distinct rows; `run_key` makes re-imports +-- idempotent: +-- run_key = sha256(program_key | analyzed_document_hash | variant | +-- variations_run | sample_type | calc_strategy | +-- sampling_strategy | max_plans | full_run | sort(include)) +-- ============================================================================= +CREATE TABLE IF NOT EXISTS analysis_runs ( + id BIGSERIAL PRIMARY KEY, + run_key TEXT UNIQUE NOT NULL, + program_key TEXT NOT NULL, -- parent program (no FK; LEFT JOIN programs) + analyzed_document_hash TEXT NOT NULL, -- hash of the exact degree analyzed (trimmed variant if trimmed) + variant TEXT NOT NULL DEFAULT 'full', -- 'full' | 'trimmed' | other transform label + trimmed BOOLEAN NOT NULL DEFAULT false, + analyzed_document JSONB, -- the analyzed degree when it differs from the program's document; NULL for full runs + + -- run parameters (nullable: a report may not record all of them) + variations_run INTEGER, + sample_type TEXT, + calc_strategy TEXT, -- mean | median + sampling_strategy TEXT, -- sequential | shuffled | stratified + max_plans INTEGER, + full_run BOOLEAN, + included_courses TEXT[], + + -- degree-level aggregate metrics + degree_metrics JSONB, -- {complexity:{min..q3}, credits:{...}, delay:{...}} + complexity_mean REAL, -- promoted from degree_metrics for ranking/filtering + delay_mean REAL, + credits_mean REAL, + + generation BIGINT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================================= +-- analysis_course_metrics — per run x course graph metrics +-- Promoted *_mean columns for cross-program ranking; full 7-stat breakdown in +-- `metrics` JSONB. Generation-swept per run. +-- ============================================================================= +CREATE TABLE IF NOT EXISTS analysis_course_metrics ( + id BIGSERIAL PRIMARY KEY, + run_key TEXT NOT NULL, + program_key TEXT NOT NULL, -- denormalized for cross-run course queries + course_code TEXT NOT NULL, + plan_count INTEGER, -- how many variations included this course + complexity_mean REAL, + centrality_mean REAL, + delay_mean REAL, + blocking_mean REAL, + metrics JSONB, -- {complexity,centrality,delay,blocking} each {min..q3} + generation BIGINT NOT NULL DEFAULT 0, + UNIQUE (run_key, course_code) +); + +-- ============================================================================= +-- analysis_plans — per run x selected exemplar plan (shortest/longest/samples) +-- Generation-swept per run. +-- ============================================================================= +CREATE TABLE IF NOT EXISTS analysis_plans ( + id BIGSERIAL PRIMARY KEY, + run_key TEXT NOT NULL, + program_key TEXT NOT NULL, + plan_index INTEGER NOT NULL, -- position within the run's selected_plans + category TEXT, -- 'Shortest Path' | 'Longest Path' | 'Random Sample' | ... + terms_required INTEGER, + total_complexity REAL, + longest_delay REAL, + credits REAL, + course_count INTEGER, + is_calc_ready BOOLEAN, + critical_path JSONB, -- [course_code, ...] + schedule JSONB, -- [{term, courses:[...], credits}, ...] + generation BIGINT NOT NULL DEFAULT 0, + UNIQUE (run_key, plan_index) +); + +-- ============================================================================= +-- Indexes +-- ============================================================================= +CREATE INDEX IF NOT EXISTS idx_programs_unitid ON programs (unitid); +CREATE INDEX IF NOT EXISTS idx_programs_cip_code ON programs (cip_code); +CREATE INDEX IF NOT EXISTS idx_programs_degree_type ON programs (degree_type); +CREATE INDEX IF NOT EXISTS idx_programs_program_kind ON programs (program_kind); +CREATE INDEX IF NOT EXISTS idx_programs_discipline ON programs (discipline); +CREATE INDEX IF NOT EXISTS idx_programs_tags ON programs USING GIN (tags); +CREATE INDEX IF NOT EXISTS idx_programs_verified ON programs (verified) WHERE verified = true; +CREATE INDEX IF NOT EXISTS idx_programs_unresolved ON programs (institution_resolved) WHERE institution_resolved = false; +CREATE INDEX IF NOT EXISTS idx_programs_impossible ON programs (has_impossible_requirements) WHERE has_impossible_requirements = true; + +CREATE INDEX IF NOT EXISTS idx_courses_prefix_number ON courses (prefix, number); +CREATE INDEX IF NOT EXISTS idx_courses_name_trgm ON courses USING GIN (name gin_trgm_ops); +CREATE INDEX IF NOT EXISTS idx_courses_unitid ON courses (unitid); +CREATE INDEX IF NOT EXISTS idx_courses_inst_ref ON courses (institution_ref); + +CREATE INDEX IF NOT EXISTS idx_program_courses_pk ON program_courses (program_key); +CREATE INDEX IF NOT EXISTS idx_program_courses_lookup ON program_courses (institution_ref, course_code); + +CREATE INDEX IF NOT EXISTS idx_program_reqs_pk ON program_requirements (program_key); +CREATE INDEX IF NOT EXISTS idx_program_reqs_parent ON program_requirements (program_key, parent_path); +CREATE INDEX IF NOT EXISTS idx_program_reqs_dblcount ON program_requirements (allow_double_count) WHERE allow_double_count = true; +CREATE INDEX IF NOT EXISTS idx_program_reqs_category ON program_requirements (category); + +CREATE INDEX IF NOT EXISTS idx_analysis_runs_program ON analysis_runs (program_key); +CREATE INDEX IF NOT EXISTS idx_analysis_runs_variant ON analysis_runs (variant); +CREATE INDEX IF NOT EXISTS idx_analysis_runs_complexity ON analysis_runs (complexity_mean); + +CREATE INDEX IF NOT EXISTS idx_acm_run ON analysis_course_metrics (run_key); +CREATE INDEX IF NOT EXISTS idx_acm_program_course ON analysis_course_metrics (program_key, course_code); +CREATE INDEX IF NOT EXISTS idx_acm_complexity ON analysis_course_metrics (complexity_mean); + +CREATE INDEX IF NOT EXISTS idx_analysis_plans_run ON analysis_plans (run_key); +CREATE INDEX IF NOT EXISTS idx_analysis_plans_program ON analysis_plans (program_key); +CREATE INDEX IF NOT EXISTS idx_analysis_plans_category ON analysis_plans (category); + +-- ============================================================================= +-- Row-Level Security +-- Mirrors the auth-required model: every read AND write needs +-- `auth.role() = 'authenticated'`. Idempotent: policies dropped before create. +-- degree_types is a static lookup (read-only for clients; seeded via exec-sql). +-- ============================================================================= +ALTER TABLE programs ENABLE ROW LEVEL SECURITY; +ALTER TABLE courses ENABLE ROW LEVEL SECURITY; +ALTER TABLE program_courses ENABLE ROW LEVEL SECURITY; +ALTER TABLE program_requirements ENABLE ROW LEVEL SECURITY; +ALTER TABLE degree_types ENABLE ROW LEVEL SECURITY; +ALTER TABLE analysis_runs ENABLE ROW LEVEL SECURITY; +ALTER TABLE analysis_course_metrics ENABLE ROW LEVEL SECURITY; +ALTER TABLE analysis_plans ENABLE ROW LEVEL SECURITY; + +-- programs +DROP POLICY IF EXISTS "auth read programs" ON programs; +CREATE POLICY "auth read programs" ON programs FOR SELECT USING (auth.role() = 'authenticated'); +DROP POLICY IF EXISTS "auth write programs" ON programs; +CREATE POLICY "auth write programs" ON programs FOR ALL USING (auth.role() = 'authenticated') WITH CHECK (auth.role() = 'authenticated'); + +-- courses +DROP POLICY IF EXISTS "auth read courses" ON courses; +CREATE POLICY "auth read courses" ON courses FOR SELECT USING (auth.role() = 'authenticated'); +DROP POLICY IF EXISTS "auth write courses" ON courses; +CREATE POLICY "auth write courses" ON courses FOR ALL USING (auth.role() = 'authenticated') WITH CHECK (auth.role() = 'authenticated'); + +-- program_courses +DROP POLICY IF EXISTS "auth read program_courses" ON program_courses; +CREATE POLICY "auth read program_courses" ON program_courses FOR SELECT USING (auth.role() = 'authenticated'); +DROP POLICY IF EXISTS "auth write program_courses" ON program_courses; +CREATE POLICY "auth write program_courses" ON program_courses FOR ALL USING (auth.role() = 'authenticated') WITH CHECK (auth.role() = 'authenticated'); + +-- program_requirements +DROP POLICY IF EXISTS "auth read program_requirements" ON program_requirements; +CREATE POLICY "auth read program_requirements" ON program_requirements FOR SELECT USING (auth.role() = 'authenticated'); +DROP POLICY IF EXISTS "auth write program_requirements" ON program_requirements; +CREATE POLICY "auth write program_requirements" ON program_requirements FOR ALL USING (auth.role() = 'authenticated') WITH CHECK (auth.role() = 'authenticated'); + +-- degree_types (read-only for clients) +DROP POLICY IF EXISTS "auth read degree_types" ON degree_types; +CREATE POLICY "auth read degree_types" ON degree_types FOR SELECT USING (auth.role() = 'authenticated'); + +-- analysis_runs +DROP POLICY IF EXISTS "auth read analysis_runs" ON analysis_runs; +CREATE POLICY "auth read analysis_runs" ON analysis_runs FOR SELECT USING (auth.role() = 'authenticated'); +DROP POLICY IF EXISTS "auth write analysis_runs" ON analysis_runs; +CREATE POLICY "auth write analysis_runs" ON analysis_runs FOR ALL USING (auth.role() = 'authenticated') WITH CHECK (auth.role() = 'authenticated'); + +-- analysis_course_metrics +DROP POLICY IF EXISTS "auth read analysis_course_metrics" ON analysis_course_metrics; +CREATE POLICY "auth read analysis_course_metrics" ON analysis_course_metrics FOR SELECT USING (auth.role() = 'authenticated'); +DROP POLICY IF EXISTS "auth write analysis_course_metrics" ON analysis_course_metrics; +CREATE POLICY "auth write analysis_course_metrics" ON analysis_course_metrics FOR ALL USING (auth.role() = 'authenticated') WITH CHECK (auth.role() = 'authenticated'); + +-- analysis_plans +DROP POLICY IF EXISTS "auth read analysis_plans" ON analysis_plans; +CREATE POLICY "auth read analysis_plans" ON analysis_plans FOR SELECT USING (auth.role() = 'authenticated'); +DROP POLICY IF EXISTS "auth write analysis_plans" ON analysis_plans; +CREATE POLICY "auth write analysis_plans" ON analysis_plans FOR ALL USING (auth.role() = 'authenticated') WITH CHECK (auth.role() = 'authenticated'); diff --git a/docs/database/setup.md b/docs/database/setup.md index 3fa5dc9..be020ce 100644 --- a/docs/database/setup.md +++ b/docs/database/setup.md @@ -111,15 +111,28 @@ The schema creates: ### Step 4b — Seed the lookup and CIP tables -After the schema, run these two files in the SQL Editor. Order matters. +After the schema, run these files in the SQL Editor. Order matters. ``` -docs/database/schema.sql ← run first (creates tables + indexes + RLS policies) -docs/database/cip-seed.sql ← run second (populates cip_codes — 2,173 CIP 2020 codes) -docs/database/lookup-seed.sql ← run third (populates award_levels, carnegie_class, locale, etc.) +docs/database/schema.sql ← run first (creates tables + indexes + RLS policies) +docs/database/cip-seed.sql ← run second (populates cip_codes — 2,173 CIP 2020 codes) +docs/database/lookup-seed.sql ← run third (populates award_levels, carnegie_class, locale, etc.) +docs/database/programs-schema.sql ← run fourth (creates the stored-programs tables — see below) +docs/database/program-lookup-seed.sql ← run fifth (seeds degree_types) ``` -All three must be done **before** importing IPEDS data. +The first three must be done **before** importing IPEDS data. The last two add +the normalized stored-programs tables; run them before `db import` / +`import_degree` (see [Stored programs (normalized)](#stored-programs-normalized)). +Each can also be applied from the CLI: + +```sh +nuanalytics db exec-sql docs/database/programs-schema.sql +nuanalytics db exec-sql docs/database/program-lookup-seed.sql +``` + +Every object in these files uses `IF NOT EXISTS` and drops policies before +recreating them, so they are safe to re-run on a live database. --- @@ -277,6 +290,72 @@ nuanalytics db logout --- +## Stored programs (normalized) + +`docs/database/programs-schema.sql` (+ `docs/database/program-lookup-seed.sql`, +applied in [Step 4b](#step-4b--seed-the-lookup-and-cip-tables)) add a +normalized, **queryable** projection of imported degree programs alongside the +lossless source document. Eight tables: + +| Table | What it holds | +|-------|---------------| +| `degree_types` | Lookup for normalized `degree_type` codes (`BS`, `BA`, `MINOR`, …); seeded by `program-lookup-seed.sql` | +| `programs` | One row per program. The lossless unified-JSON `document` (JSONB) is the source of truth; the other columns are a queryable scalar projection. `program_key` is the deterministic idempotency key | +| `courses` | Shared per-institution course catalog, partitioned by `institution_ref` and keyed `(institution_ref, course_code)` | +| `program_courses` | M:N junction programs ↔ courses, with per-program `credit_hours_override` / `name_as_listed` | +| `program_requirements` | The requirement tree flattened by `req_path` / `parent_path`, with JSONB `selection_spec` / `req_constraints` and `is_impossible` / `allow_double_count` flags | +| `analysis_runs` | One row per `degree analyze` run of a program: `variant`, `trimmed`, `variations_run`, `sample_type`, the `degree_metrics` JSONB, plus promoted `complexity_mean` / `delay_mean` / `credits_mean` | +| `analysis_course_metrics` | Per run × course metric breakdown (promoted `*_mean` columns + full JSONB) | +| `analysis_plans` | Per run × selected exemplar plan (shortest / longest / sample) | + +Design mirrors the IPEDS tables: rows link by **natural keys** (`program_key`, +`(institution_ref, course_code)`) with no cross-table foreign keys, RLS gates +every read and write on `auth.role() = 'authenticated'`, and a per-import +`generation` stamp makes re-syncing idempotent (the `programs` row is the commit +marker, bumped last). + +### Importing programs + +Once the tables exist and you're signed in, load degree reports with the CLI +`db import` command (or the `import_degree` MCP tool). A degree-first analysis +report (`*_report.json`) populates the program projection and, when it carries +an `analysis` block, one analysis run; a plain unified/YAML degree populates +just the program. The institution is resolved against `institutions` (the +report's `unitid`, then a name + CIP lookup); an ambiguous name lists candidate +institutions and writes nothing. + +```sh +# Import one report +nuanalytics db import metrics/neu-khoury-bscs-boston_report.json + +# Preview a directory of reports without writing +nuanalytics db import metrics/ --dry-run + +# Pin the institution and overwrite an existing (unverified) program +nuanalytics db import report.json --unitid 167358 --replace +``` + +`--dry-run` reports the row counts without writing: + +``` +ℹ .json: skipped (dry-run) + institution: Colorado State University (unitid 126818) + program_key: prog:126818|11.0701|2025-2026|BS + variant: full + analysis: 10000 variations (shuffled), 7 sample plans + rows: 82 courses, 32 requirements, 72 course-metrics +``` + +Overwriting an existing program requires `--replace` (unverified) or `--force` +(verified). Stored programs can then be analyzed without re-reading the file via +`degree analyze --from-db ` (see [Degree Command](../degree.md#analyze-a-stored-program---from-db)). +The full flag set (`--variant`, `--unitid`, `--institution`, `--cip`, +`--catalog`, `--degree-id`, `--force`, `--replace`, `--skip-existing`, +`--dry-run`, `-j/--jobs`) is available via `nuanalytics db import --help`; the +[MCP `import_degree` tool](../mcp.md#import_degree) exposes the same options. + +--- + ## Configuration reference | Config key | Description | Example | diff --git a/docs/degree-ingestion-plan.md b/docs/degree-ingestion-plan.md index 359b6d1..16d18ff 100644 --- a/docs/degree-ingestion-plan.md +++ b/docs/degree-ingestion-plan.md @@ -1,5 +1,10 @@ # Degree YAML Ingestion Implementation Plan +> **Status:** the "future database storage" this plan anticipates is now +> implemented — degrees import into the normalized program tables via the CLI +> `db import` command and the `import_degree` MCP tool. See +> [Database Setup → Stored programs (normalized)](database/setup.md#stored-programs-normalized). + ## Overview Add degree YAML parsing and validation without plan extraction. Load degrees via CLI and test the structure. Incremental delivery focused on reading YAML, validating structure, and ensuring proper data representation for future database storage. diff --git a/docs/degree.md b/docs/degree.md index 060d84e..7c154de 100644 --- a/docs/degree.md +++ b/docs/degree.md @@ -80,6 +80,9 @@ This generates: | `--include ` | Courses to always include in all plans (comma-separated) | none | | `-j, --jobs ` | Files to analyze concurrently when multiple are given, each in its own process (see [Parallel analysis](#parallel-analysis)) | 8 | | `--school ` | Treat all inputs as programs of one school and also emit a combined `_school_report.json` rollup | none | +| `--from-db ` | Analyze a stored program fetched from the database instead of a file (see [Analyze a stored program](#analyze-a-stored-program---from-db)). Mutually exclusive with positional `FILES` | none | + +`FILES` is optional when `--from-db` is given; otherwise at least one file is required. **Examples:** @@ -152,6 +155,42 @@ console output. For an externally throttled variant (a per-process `ulimit -v` memory cap and a timeout), see `scripts/analyze-batch.sh`; the in-process pool deliberately imposes no ulimit or timeout. +### Analyze a stored program (`--from-db`) + +`--from-db ` analyzes a program already imported into the database +(via `db import` or the `import_degree` MCP tool — see +[Database Setup](database/setup.md)) instead of a local file. The stored +program's lossless `document` is parsed back into a degree and analyzed with +exactly the same options as the file-based path. This is single-program only — +no worker pool, so `-j/--jobs` doesn't apply. + +`` is resolved through a ladder; the first tier that matches wins: + +1. exact `program_key` (e.g. `prog:167358|11.0701|2025-2026|BS`) +2. exact `degree_id` +3. a `name` substring (case-insensitive `ILIKE`) + +Exactly one match is analyzed; zero matches is an error; multiple matches list +the candidates as `program_key · name · institution · catalog_year` so you can +re-run with a more specific name or the exact `program_key`. + +```bash +# Analyze a stored program by name +nuanalytics degree analyze --from-db "Computer Science (Boston)" + +# Or pin the exact program by key, with the usual analyze options +nuanalytics degree analyze --from-db "prog:167358|11.0701|2025-2026|BS" --sample-plans 20 +``` + +On a single match the loaded-program banner is printed before analysis: + +``` +✓ Loaded stored program: Bachelor of Science in Computer Science (Boston) (program_key prog:167358|11.0701|2025-2026|BS · unitid 167358) +``` + +`--from-db` requires a configured, logged-in database session +(`nuanalytics db login`). It is mutually exclusive with positional `FILES`. + ### Validation (`validate`) ```bash diff --git a/docs/json-dump-feedback.md b/docs/json-dump-feedback.md deleted file mode 100644 index d047c77..0000000 --- a/docs/json-dump-feedback.md +++ /dev/null @@ -1,209 +0,0 @@ -# AI-Landscape JSON Dump — Review & Feedback - -_Review of the `degree analyze` JSON output (`.debug/metrics/json-dump/`) produced -from the ai-landscape `cluster_outputs/` scrape, cross-checked against the -human-validated set (`validation_jsons_stripped/`). Date: 2026-06-03._ - -## What was reviewed - -| | count | -|---|---| -| Converted unified programs (from `cluster_outputs/`, 643 pipeline files) | 1,033 | -| Reports successfully produced (scraped) | 930 | -| Scraped programs that **failed** (OOM / SIGKILL) | 35 | -| Validated programs converted + analyzed (from `validation_jsons_stripped/`) | 42 | -| **Reports now in `json-dump/` (scraped + validated merged)** | **972** | - -The 42 validated reports were generated and merged into the dump (see -[§5](#5-validated-set-merged-into-the-dump)). They are **additions**, not -overwrites — see [§2.5](#25-scraped-vs-validated-naming-divergence). - ---- - -## 1. NuAnalytics engine issues (these block the dump's usefulness) - -These two are **our** bugs, not ai-landscape's. They are the reason the dump is -currently low-value, and they should be fixed before another full run. - -### 1.1 OOM on large elective pools — *eager combination materialization* - -**All 35 failures are `signal: 9 (SIGKILL)` = out-of-memory.** The cause is **not** -catalog size and **not** plan enumeration — it is that -`RequirementResolver::resolve_select_requirement` calls `generate_combinations(pool, count)`, -which **eagerly materializes every C(N, k) combination into a `Vec>`**, -for *every* `select` requirement — including `category: "elective"` ones that are -later discarded from the plan space (see §1.2). - -A program with one large "choose k of N" elective explodes regardless of how few -courses it has: - -| Program | Courses | Largest `choose k of N` | C(N,k) | Result | -|---|---|---|---|---| -| Iowa State CS BS | **78** | choose 15 of 42 | 9.9×10¹⁰ | **OOM (>6 GB)** | -| Missouri-Columbia | 64 | choose 24 of 41 | 1.5×10¹¹ | OOM | -| UW-La Crosse | 64 | choose 12 of 51 | 1.6×10¹¹ | OOM | -| USF AI | 16,012 | choose 6 of 16,000 | 2.3×10²² | OOM | - -**Proof it's the combination Vec, not the course graph:** Iowa State (78 courses) -peaks at **>6 GB and aborts**. Shrinking just that one requirement to "choose 2 of 42" -(C=861) drops peak memory to **22 MB** and it completes. The 78-course graph is -trivial; the 9.9×10¹⁰-element `Vec` is the hog. - -`--jobs 8` (the default pool) then stacks ~8 of these allocations on a 15 GiB box, -so the OS OOM-killer fires and takes out multiple concurrent workers — including -small innocent ones running at the same time. - -**Recommended fix (NuAnalytics):** never materialize the full combination set. -Either (a) sample `min(C(N,k), max_plans)` combinations lazily, or (b) cap the -materialized set and warn, or (c) skip materialization entirely for -non-enumerable categories (since electives are discarded anyway — see §1.2). Option -(c) alone would have prevented all 35 failures. - -### 1.2 Every report has `variations_run: 1` — *degenerate, single-plan metrics* - -**All 971 reports** (min = median = max = 1) analyze exactly **one** plan, so every -degree-level distribution is collapsed: `std_dev = 0`, and -`median = mean = min = max = q1 = q3`. The whole point of the metrics output -(variations, spread) is lost. - -**Root cause:** `plan_generator.rs` enumerates only requirements whose category is in -`ENUMERABLE_CATEGORIES = ["major"]`. The converter labels **every** picklist/elective -`select` requirement `category: "elective"`, so none of a converted program's choice -points ever enter the plan space → exactly 1 plan. - -**Proof:** flipping American University's picklist categories `elective → major` takes -it from `Estimated total plans: 1` to **74,256 estimated / 986 analyzed, complexity -std_dev 7.25** — a real distribution. - -> Note the tension with §1.1: simply enabling enumeration of electives would -> immediately trigger the OOM in §1.1 for big pools. **Both must be fixed together** — -> enumerate elective choice points *and* sample combinations instead of materializing -> them. - -**What is still meaningful today:** the **per-course** metrics (complexity, centrality, -blocking, delay) are derived from the prerequisite graph, not from plan sampling, so -they are valid even at `variations_run = 1`. Only the degree-level distributions are -degenerate. (E.g. CSU Fullerton: 26/31 courses carry non-zero graph metrics.) - ---- - -## 2. ai-landscape data-quality issues - -### 2.1 Picklist `[N]` is ambiguous: course-count vs. credits → 316 impossible requirements - -`[N]` in a picklist tag is interpreted inconsistently in the source data: - -- `"Calculus [1]"` over 2 courses → clearly **choose 1 course**. -- `"Artificial Intelligence Concentration [12]"` over **5** courses → **impossible** as - a course count (can't choose 12 of 5); only makes sense as **12 credits** (≈ 4 courses). - -Because the converter treats `[N]` as a course count, **316 `select` requirements across -225 of 1,033 files (20%)** are emitted with `count > pool_size`, which are unsatisfiable. - -**Recommendation (ai-landscape):** make `[N]` unambiguous — either always "number of -courses" or always "number of credits", or tag it explicitly -(e.g. `[3 courses]` vs `[12 credits]`). This is the single highest-value data fix. -(NuAnalytics should also defensively clamp `count` to the pool size.) - -### 2.2 Unparseable picklist tags (missing `[N]`) — 481+ occurrences - -The most common conversion warning is **`Unparseable picklist tag`**: a picklist label -with no `[N]` annotation at all, so no choice count can be derived. Top offenders: - -``` -481× "Computer Science Electives …" -136× "Technical Electives …" -101× "CSC 300-400 level …" - 85× "Artificial Intelligence …" - 77× "Upper Division …" -``` - -**Recommendation (ai-landscape):** every picklist tag should carry the `[N]` count, or -be omitted if it isn't actually a choice group. - -### 2.3 Catalog-scrape outliers (whole-catalog ingestion) - -A handful of programs captured far more courses than a degree could contain — the -scraper appears to have swallowed an entire course catalog: - -``` -16,012 University of South Florida — Artificial Intelligence (B.S.A.I.) - 2,046 Kean University — Artificial Intelligence - 1,991 University of Vermont — B.A. in Computer Science - 1,926 CUNY Brooklyn College — Computer Science, B.S. - 1,031 University of North Florida — Computer Science (BS) -``` -(Median program course count is **29**.) These are almost certainly scraper errors and -should be flagged/re-scraped. - -### 2.4 Missing `course_hours` → defaulted to 3 credits - -Of 1,033 converted files, **452 carry conversion warnings (11,162 total)**; a large -share are `missing course_hours; assuming 3 credits`. The 3-credit assumption is -reasonable but silently skews credit totals. **Recommendation:** populate -`course_hours` (we can also backfill from the IPEDS/Supabase catalog before defaulting). - -### 2.5 Scraped vs. validated naming divergence - -The scraped and validated records for the *same* program use different degree-name -strings (scraped: `"Computer Science, B.S."`; validated: -`"Bachelor's of Science Computer Science"`), so their report filenames never collide. -23 of the 41 validated universities also appear in the scraped set, but as -differently-named files. **Recommendation:** converge on a canonical -`university` + `degree` + `degree_type` identity so records can be matched and de-duplicated. - ---- - -## 3. What's working well - -- **Conversion is robust:** all 1,033 programs converted without crashing; the cluster - pipeline format (`course_verifier..results`) is correctly unwrapped. -- **Prerequisites** import losslessly into the tagged `{and|or}` AST. -- **Tags** (`ai`, `ai-major`/`ai-concentration`/`ai-minor`) are carried onto the degree - and surface correctly in reports. -- **Per-course graph metrics** are sound and useful today (see §1.2). -- The validated set is clean and bounded (max branching 4.1×10⁴ vs the failed-set - median 3.4×10⁸) — **human validation already fixes the elective explosion.** - ---- - -## 4. Recommendations, prioritized - -**NuAnalytics (we own these):** -1. **Stop materializing combinations** in `generate_combinations` (fixes all 35 OOMs). — §1.1 -2. **Enumerate elective choice points**, not just `category == "major"` (fixes - `variations_run = 1`). Must land with #1. — §1.2 -3. Defensively **clamp `select.count` to the pool size** + warn. — §2.1 - -**ai-landscape (data):** -1. Disambiguate picklist **`[N]` (courses vs credits)**. — §2.1 -2. Ensure every **picklist tag carries `[N]`**. — §2.2 -3. Fix **whole-catalog scrapes** (USF, Kean, UVM, CUNY Brooklyn, UNF). — §2.3 -4. Populate **`course_hours`**. — §2.4 -5. Emit a **canonical program identity** for matching/dedup. — §2.5 - ---- - -## 5. Validated set merged into the dump - -The 42 `validation_jsons_stripped/` programs were converted -(`degree convert … -o .debug/converted_validated/`) and analyzed -(`degree analyze … -j 4`, 0 failures) into the dump. Notably, the **scraped** -CSU-Fullerton CS BS was one of the 35 OOM failures, while its **validated** version -analyzes cleanly — a concrete win for using validated data. - -The ai-landscape directories under `ai-landscape-tools/` were **not modified**. - -## 6. Sample reports - -Five reports were generated with the tool into `.debug/metrics/json-reports/` -(report JSON + HTML + plan CSVs), anchored on the requested CSU CS BS: - -1. California State University, Fullerton — CS, B.S. _(the CSU csbs)_ -2. Northeastern University — CS concentration -3. Florida International University — CS, B.S. -4. Drexel University — CS (AI concentration) -5. Georgia Institute of Technology — CS minor - -All five are valid JSON with populated per-course metrics; all show -`variations_run: 1` per §1.2. diff --git a/docs/mcp.md b/docs/mcp.md index 4599964..18ac45f 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -499,6 +499,79 @@ picks the right tool and call shape automatically. The descriptions on each tool's `#[tool]` attribute in `src/mcp/server.rs` are the authoritative source — Claude reads them at handshake time. +> **Resolving a UNITID.** `search_institutions(name=…)` returns the +> `unitid` to pass to `import_degree` (or the CLI `db import --unitid`). The +> name match is a case-insensitive substring, so narrow an ambiguous name with +> `state` (e.g. `search_institutions(name="Boston", state="MA")`). + +### `import_degree` + +Imports a degree-first analysis report (or a plain unified degree) into the +normalized program tables — the MCP counterpart of the CLI `db import` command. +One report populates the program projection (`programs`, `courses`, +`program_courses`, `program_requirements`) plus, when it carries an `analysis` +block, one analysis run with its course metrics and selected plans. The +institution is resolved against IPEDS (the report's `unitid`, then a name + CIP +lookup); an ambiguous institution name returns the candidate `(unitid, name)` +pairs so the agent can re-call with an explicit `unitid`. + +**Parameters:** +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `json_content` | string | One of `json_content` / `json_path` | Inline degree-first report (or unified degree) JSON | +| `json_path` | string | One of `json_content` / `json_path` | Path to a report JSON file on the MCP server's filesystem | +| `variant` | string | No | Analysis-run variant label (default `"full"`). `"full"` writes the program projection; a non-full variant only attaches an analysis run | +| `unitid` | integer | No | Override the resolved IPEDS UNITID — set this to disambiguate after an `institution_ambiguous` result | +| `institution` | string | No | Override the institution name used for resolution / display | +| `cip` | string | No | Override the CIP code (part of the natural program key) | +| `catalog` | string | No | Override the catalog year (part of the program identity) | +| `degree_id` | string | No | Override the degree id | +| `force` | boolean | No | Overwrite a verified program / skip the confirmation gate | +| `replace` | boolean | No | Replace an existing (unverified) program | +| `skip_existing` | boolean | No | Skip the program entirely if it already exists | +| `dry_run` | boolean | No | Preview the import (report counts) without writing anything | + +**Response Format:** +```json +{ + "result": "created", + "program_key": "prog:167358|11.0701|2025-2026|BS", + "resolved_unitid": 167358, + "institution": "Northeastern University", + "variant": "full", + "variations_run": 10000, + "sample_type": "shuffled", + "courses_written": 82, + "requirements_written": 32, + "run_written": true, + "plans_written": 7, + "course_metrics_written": 72, + "conversion_warnings": [], + "messages": ["..."] +} +``` + +`result` is a stable lowercase tag: `created` | `updated` | `skipped` | +`needs_confirmation` | `institution_ambiguous` | `rejected`. The blocked +variants attach an extra payload: + +- `institution_ambiguous` → `institution_candidates: [{ "unitid": …, "name": … }]` + (re-call with one as `unitid`) +- `needs_confirmation` → `reason` (the program exists / is verified; re-call + with `replace` or `force`) +- `rejected` → `errors` (the report could not be turned into a valid plan) + +**Notes:** +- DB-gated — registered only with a logged-in session, like the other + database-backed tools. +- `dry_run: true` runs the full resolution + plan build and returns the row + counts, but writes nothing. + +**Example Prompts:** +- "Import this analysis report into the database" → calls with `json_path: "…"` +- "Preview importing this degree without writing" → calls with `dry_run: true` +- "That matched two schools — use UNITID 167358" → re-calls with `unitid: 167358` + ## Testing ### Using MCP Inspector (Recommended) diff --git a/samples/degrees/csu-cs-bscs-general_report.json b/samples/degrees/csu-cs-bscs-general_report.json new file mode 100644 index 0000000..6945d40 --- /dev/null +++ b/samples/degrees/csu-cs-bscs-general_report.json @@ -0,0 +1,5542 @@ +{ + "degree": { + "catalog_year": "2025-2026", + "cip_code": "11.0701", + "degree_type": "BS", + "id": "colorado-state-university-fort-collins-cs-concentration", + "institution": "Colorado State University", + "major_subjects": [ + "CS", + "CT", + "DSCI", + "MATH", + "STAT" + ], + "name": "Computer Science, BS (Computer Science Concentration)", + "source_url": "https://catalog.colostate.edu/general-catalog/colleges/natural-sciences/computer-science/computer-science-major/computer-science-concentration/", + "system_type": "semester", + "tags": [ + "cs", + "cs-major" + ], + "total_credits": 120 + }, + "analysis": { + "metrics": { + "complexity": { + "max": 268.0, + "mean": 198.41330000000005, + "median": 195.0, + "min": 155.0, + "q1": 185.0, + "q3": 209.0, + "std_dev": 19.276869121047664 + }, + "credits": { + "max": 133.0, + "mean": 121.57659999999996, + "median": 121.57659999999996, + "min": 120.0, + "q1": 120.0, + "q3": 123.57443193487336, + "std_dev": 1.9978319348734035 + }, + "delay": { + "max": 7.0, + "mean": 6.0483, + "median": 6.0, + "min": 5.0, + "q1": 6.0, + "q3": 6.0, + "std_dev": 0.6188433646731596 + } + }, + "sample_type": "shuffled", + "variations_run": 10000 + }, + "requirements": { + "advanced_writing_aucc_2": { + "category": "gen_ed", + "credits": 3, + "from": { + "include": [ + "*:*" + ] + }, + "name": "Advanced Writing (AUCC 2)", + "tags": [ + "gen_ed", + "writing" + ], + "type": "select" + }, + "arts_humanities_aucc_3b": { + "category": "gen_ed", + "credits": 3, + "from": { + "include": [ + "*:*" + ] + }, + "name": "Arts and Humanities (AUCC 3B)", + "tags": [ + "gen_ed" + ], + "type": "select" + }, + "capstone_course": { + "category": "major", + "constraints": { + "exclude_used": true + }, + "count": 1, + "from": { + "courses": [ + "CS410", + "CS414", + "CS420", + "CS425", + "CS430", + "CS435", + "CS440", + "CS445", + "CS453", + "CS454", + "CS455", + "CS456", + "CS457", + "CS458", + "CS462", + "CS464", + "CS465", + "CS470", + "CS475" + ] + }, + "name": "Capstone Course (one CS 400-level capstone)", + "tags": [ + "capstone", + "required" + ], + "type": "select" + }, + "computer_systems_course": { + "category": "major", + "count": 1, + "from": { + "courses": [ + "CS250", + "CS270" + ] + }, + "name": "Computer Systems Course", + "tags": [ + "core" + ], + "type": "select" + }, + "cs_300_electives": { + "category": "major", + "constraints": { + "exclude_used": true + }, + "count": 2, + "from": { + "courses": [ + "CS310H", + "CS312", + "CS345", + "CS356" + ] + }, + "name": "Two CS Courses 300-379 (excluding required core, 380-399)", + "tags": [ + "elective" + ], + "type": "select" + }, + "cs_400_electives": { + "category": "major", + "constraints": { + "exclude_used": true + }, + "count": 2, + "from": { + "courses": [ + "CS410", + "CS414", + "CS415", + "CS420", + "CS422", + "CS425", + "CS430", + "CS435", + "CS440", + "CS445", + "CS453", + "CS454", + "CS455", + "CS456", + "CS457", + "CS458", + "CS462", + "CS464", + "CS465", + "CS470", + "CS475" + ] + }, + "name": "Two CS Courses 400-479 (excluding 480-499)", + "tags": [ + "elective" + ], + "type": "select" + }, + "cs_data_structures": { + "category": "major", + "courses": [ + "CS165" + ], + "name": "CS2 - Data Structures", + "tags": [ + "core" + ], + "type": "all" + }, + "cs_upper_core": { + "category": "major", + "courses": [ + "CS314", + "CS320", + "CS370" + ], + "name": "Upper-Division Core (Software Engineering, Algorithms, Operating Systems)", + "tags": [ + "core" + ], + "type": "all" + }, + "discrete_structures": { + "category": "major", + "courses": [ + "CS220" + ], + "name": "Discrete Structures and the Applications", + "tags": [ + "core" + ], + "type": "all" + }, + "diversity_aucc_1c": { + "category": "gen_ed", + "credits": 3, + "from": { + "include": [ + "*:*" + ] + }, + "name": "Diversity and Global Perspectives (AUCC 1C)", + "tags": [ + "gen_ed" + ], + "type": "select" + }, + "ethics": { + "category": "major", + "courses": [ + "CS201" + ], + "name": "Ethical Computing Systems (AUCC 3B / GT-AH3)", + "tags": [ + "required" + ], + "type": "all" + }, + "first_year_composition": { + "category": "gen_ed", + "courses": [ + "CO150" + ], + "name": "College Composition (AUCC 1A)", + "tags": [ + "gen_ed", + "writing" + ], + "type": "all" + }, + "free_electives": { + "category": "elective", + "credits": 20, + "from": { + "include": [ + "*:*" + ] + }, + "name": "Unrestricted Electives", + "tags": [ + "elective" + ], + "type": "select" + }, + "freshman_programming_group": { + "category": "major", + "name": "Freshman Programming Sequence (choose one group)", + "options": [ + { + "id": "group_a", + "name": "Group A: Culture & Coding + CS1", + "requirements": [ + { + "count": 1, + "from": { + "courses": [ + "CS150A", + "CS150B" + ] + }, + "name": "Culture and Coding (CS 150A or 150B)", + "type": "select" + }, + { + "count": 1, + "from": { + "courses": [ + "CS162", + "CS164" + ] + }, + "name": "CS1 (CS 162 or 164)", + "type": "select" + } + ] + }, + { + "id": "group_b", + "name": "Group B: Python for STEM + CS1", + "requirements": [ + { + "courses": [ + "CS152" + ], + "name": "Python for STEM (CS 152)", + "type": "all" + }, + { + "count": 1, + "from": { + "courses": [ + "CS162", + "CS164" + ] + }, + "name": "CS1 (CS 162 or 164)", + "type": "select" + } + ] + }, + { + "id": "group_c", + "name": "Group C: CS1 - No Prior Programming Experience", + "requirements": [ + { + "courses": [ + "CS163" + ], + "name": "CS1 (CS 163)", + "type": "all" + } + ] + } + ], + "tags": [ + "core" + ], + "type": "one_of" + }, + "historical_aucc_3d": { + "category": "gen_ed", + "credits": 3, + "from": { + "include": [ + "*:*" + ] + }, + "name": "Historical Perspectives (AUCC 3D)", + "tags": [ + "gen_ed" + ], + "type": "select" + }, + "linear_algebra_course": { + "category": "supporting", + "count": 1, + "from": { + "courses": [ + "DSCI369", + "MATH369" + ] + }, + "name": "Linear Algebra Course", + "tags": [ + "math", + "required" + ], + "type": "select" + }, + "math_calculus": { + "category": "supporting", + "count": 1, + "from": { + "courses": [ + "MATH156", + "MATH160" + ] + }, + "name": "Mathematics for Computational Science / Calculus (AUCC 1B / GT-MA1)", + "tags": [ + "math", + "required" + ], + "type": "select" + }, + "natural_science_aucc_3a": { + "category": "gen_ed", + "credits": 7, + "from": { + "courses": [ + "PH141", + "PH142", + "CHEM111", + "CHEM112", + "LIFE102", + "LIFE103" + ] + }, + "name": "Biological and Physical Sciences (AUCC 3A, with lab)", + "tags": [ + "gen_ed", + "science" + ], + "type": "select" + }, + "senior_focus_group": { + "category": "major", + "name": "Senior Focus (Technology Focus or Minor / Second Major)", + "options": [ + { + "id": "technology_focus", + "name": "Group A: Technology Focus Electives + an additional CS 400+ course", + "requirements": [ + { + "constraints": { + "exclude_used": true + }, + "credits": 6, + "from": { + "courses": [ + "BZ350", + "BZ360", + "CIS320", + "ECE452", + "ENGR422", + "JTC372", + "JTC472", + "MATH161", + "MATH256", + "MGT330", + "MGT340", + "MGT420", + "PHIL410", + "PHIL411", + "PHIL415", + "PSY252", + "PSY352", + "PSY452", + "PSY454", + "PSY456", + "PSY458" + ], + "exclude": [ + "CT301", + "DSCI369", + "MATH369", + "STAT301", + "STAT302A", + "STAT307", + "STAT315" + ], + "include": [ + "CS:300-479", + "CT:300-479", + "DSCI:300-479", + "MATH:300-479", + "STAT:300-479", + "IDEA:300-479" + ] + }, + "name": "Technology Focus Electives (6 credits)", + "type": "select" + }, + { + "constraints": { + "exclude_used": true + }, + "count": 1, + "from": { + "courses": [ + "CS410", + "CS414", + "CS415", + "CS420", + "CS422", + "CS425", + "CS430", + "CS435", + "CS440", + "CS445", + "CS453", + "CS454", + "CS455", + "CS456", + "CS457", + "CS458", + "CS462", + "CS464", + "CS465", + "CS470", + "CS475" + ] + }, + "name": "Additional CS 400-479 course", + "type": "select" + } + ] + }, + { + "id": "minor_second_major", + "name": "Group B: Minor or Second Major (10 credits)", + "requirements": [ + { + "credits": 10, + "from": { + "include": [ + "*:*" + ] + }, + "name": "Minor / Second Major coursework", + "type": "select" + } + ] + } + ], + "tags": [ + "required" + ], + "type": "one_of" + }, + "social_behavioral_aucc_3c": { + "category": "gen_ed", + "credits": 3, + "from": { + "include": [ + "*:*" + ] + }, + "name": "Social and Behavioral Sciences (AUCC 3C)", + "tags": [ + "gen_ed" + ], + "type": "select" + }, + "software_development_group": { + "category": "major", + "name": "Software Development (choose one group)", + "options": [ + { + "id": "sd_group_a", + "name": "Group A: Software Development + C++ Fundamentals", + "requirements": [ + { + "courses": [ + "CS214", + "CT301" + ], + "name": "CS 214 and CT 301", + "type": "all" + } + ] + }, + { + "id": "sd_group_b", + "name": "Group B: Software Development with C++", + "requirements": [ + { + "courses": [ + "CS253" + ], + "name": "CS 253", + "type": "all" + } + ] + } + ], + "tags": [ + "core" + ], + "type": "one_of" + }, + "statistics_course": { + "category": "supporting", + "count": 1, + "from": { + "courses": [ + "STAT301", + "STAT302A", + "STAT307", + "STAT315" + ] + }, + "name": "Statistics Course", + "tags": [ + "math", + "required" + ], + "type": "select" + } + }, + "selected_plans": [ + { + "category": "Shortest Path", + "course_count": 37, + "credits": 120.0, + "critical_path": [ + "CS150A", + "CS163", + "CS270", + "CS370", + "CS455" + ], + "is_calc_ready": false, + "longest_delay": 5, + "schedule": [ + { + "courses": [ + "CS150A", + "MATH156", + "PSY252", + "FE02", + "HA301" + ], + "credits": 16.0, + "term": 1 + }, + { + "courses": [ + "CS163", + "DSCI369", + "PSY452", + "FE05" + ], + "credits": 14.0, + "term": 2 + }, + { + "courses": [ + "CS165", + "CS270", + "CS220", + "FE06" + ], + "credits": 15.0, + "term": 3 + }, + { + "courses": [ + "CS253", + "CS370", + "CS312", + "CS320", + "LIFE102" + ], + "credits": 17.0, + "term": 4 + }, + { + "courses": [ + "CS310H", + "CS314", + "CS430", + "CS435" + ], + "credits": 14.0, + "term": 5 + }, + { + "courses": [ + "CS455", + "CS462", + "ELEC002S", + "FE03", + "SBA301" + ], + "credits": 16.0, + "term": 6 + }, + { + "courses": [ + "AHA301", + "CHEM111", + "ELEC001", + "FE04", + "STAT302A" + ], + "credits": 14.0, + "term": 7 + }, + { + "courses": [ + "AWA201", + "CS201", + "DA101", + "FE01", + "FE07S" + ], + "credits": 14.0, + "term": 8 + } + ], + "terms_required": 8, + "total_complexity": 155 + }, + { + "category": "Longest Path", + "course_count": 40, + "credits": 129.0, + "critical_path": [ + "MATH156", + "CS152", + "CS164", + "CS165", + "CS253", + "CS356", + "CS456" + ], + "is_calc_ready": false, + "longest_delay": 7, + "schedule": [ + { + "courses": [ + "MATH156", + "STAT302A", + "AHA301", + "CS201", + "FE02", + "HA301" + ], + "credits": 17.0, + "term": 1 + }, + { + "courses": [ + "CS152", + "DSCI369", + "AWA201", + "DA101", + "FE04" + ], + "credits": 15.0, + "term": 2 + }, + { + "courses": [ + "CS164", + "CS162", + "CHEM111", + "FE01", + "FE05" + ], + "credits": 16.0, + "term": 3 + }, + { + "courses": [ + "CS165", + "CS220", + "CS270", + "CT301", + "LIFE102" + ], + "credits": 18.0, + "term": 4 + }, + { + "courses": [ + "CS253", + "CS345", + "CS320", + "CS214", + "FE06" + ], + "credits": 16.0, + "term": 5 + }, + { + "courses": [ + "CS356", + "CS310H", + "CS314", + "CS425", + "FE07S" + ], + "credits": 15.0, + "term": 6 + }, + { + "courses": [ + "CS456", + "CS435", + "CS454", + "CS312" + ], + "credits": 15.0, + "term": 7 + }, + { + "courses": [ + "CS462", + "CS475", + "CS370", + "FE03", + "SBA301" + ], + "credits": 17.0, + "term": 8 + } + ], + "terms_required": 8, + "total_complexity": 268 + }, + { + "category": "Random Sample", + "course_count": 37, + "credits": 121.0, + "critical_path": [ + "CS150B", + "CS164", + "CS220", + "CS345", + "CS445" + ], + "is_calc_ready": false, + "longest_delay": 5, + "schedule": [ + { + "courses": [ + "CS150B", + "MATH156", + "STAT302A", + "FE01", + "FE05", + "SBA301" + ], + "credits": 17.0, + "term": 1 + }, + { + "courses": [ + "CS164", + "DSCI369", + "FE02", + "FE06" + ], + "credits": 14.0, + "term": 2 + }, + { + "courses": [ + "CS165", + "CS220", + "CS270", + "FE07S" + ], + "credits": 14.0, + "term": 3 + }, + { + "courses": [ + "CS253", + "CS345", + "CS320", + "CS312", + "PSY252" + ], + "credits": 16.0, + "term": 4 + }, + { + "courses": [ + "CS314", + "CS356", + "CS410", + "CS420" + ], + "credits": 14.0, + "term": 5 + }, + { + "courses": [ + "CS422", + "CS425", + "CS445", + "CS370" + ], + "credits": 15.0, + "term": 6 + }, + { + "courses": [ + "AHA301", + "CHEM111", + "ELEC001S", + "FE03", + "HA301" + ], + "credits": 15.0, + "term": 7 + }, + { + "courses": [ + "AWA201", + "CS201", + "DA101", + "FE04", + "LIFE102" + ], + "credits": 16.0, + "term": 8 + } + ], + "terms_required": 8, + "total_complexity": 186 + }, + { + "category": "Random Sample", + "course_count": 39, + "credits": 125.0, + "critical_path": [ + "CS150A", + "CS163", + "CS165", + "CS214", + "CS356", + "CS456" + ], + "is_calc_ready": false, + "longest_delay": 6, + "schedule": [ + { + "courses": [ + "CS150A", + "MATH156", + "DA101", + "FE03", + "HA301" + ], + "credits": 16.0, + "term": 1 + }, + { + "courses": [ + "CS163", + "DSCI369", + "STAT315", + "FE04", + "LIFE102" + ], + "credits": 18.0, + "term": 2 + }, + { + "courses": [ + "CS165", + "CS220", + "CS270", + "CS162", + "SBA301" + ], + "credits": 17.0, + "term": 3 + }, + { + "courses": [ + "CS214", + "CS370", + "CS320", + "CS345", + "ECE452" + ], + "credits": 15.0, + "term": 4 + }, + { + "courses": [ + "CS356", + "CS310H", + "CS314", + "CS440", + "CT301" + ], + "credits": 15.0, + "term": 5 + }, + { + "courses": [ + "CS456", + "CS457", + "CS462", + "FE06" + ], + "credits": 15.0, + "term": 6 + }, + { + "courses": [ + "CS470", + "CHEM111", + "FE01", + "FE05", + "STAT302A" + ], + "credits": 15.0, + "term": 7 + }, + { + "courses": [ + "AHA301", + "AWA201", + "CS201", + "FE02", + "FE07S" + ], + "credits": 14.0, + "term": 8 + } + ], + "terms_required": 8, + "total_complexity": 211 + }, + { + "category": "Random Sample", + "course_count": 37, + "credits": 120.0, + "critical_path": [ + "CS150A", + "CS163", + "CS165", + "CS253", + "CS314", + "CS415" + ], + "is_calc_ready": false, + "longest_delay": 6, + "schedule": [ + { + "courses": [ + "CS150A", + "MATH156", + "STAT302A", + "FE01", + "FE05", + "SBA301" + ], + "credits": 17.0, + "term": 1 + }, + { + "courses": [ + "CS163", + "DSCI369", + "FE02", + "FE06" + ], + "credits": 14.0, + "term": 2 + }, + { + "courses": [ + "CS165", + "CS220", + "CS270", + "CS162" + ], + "credits": 14.0, + "term": 3 + }, + { + "courses": [ + "CS253", + "CS312", + "CS320", + "CS345", + "HA301" + ], + "credits": 16.0, + "term": 4 + }, + { + "courses": [ + "CS314", + "CS310H", + "CS410", + "CS462" + ], + "credits": 14.0, + "term": 5 + }, + { + "courses": [ + "CS414", + "CS415", + "CS370", + "ECE452" + ], + "credits": 14.0, + "term": 6 + }, + { + "courses": [ + "AHA301", + "CHEM111", + "ELEC001", + "FE04", + "LIFE102" + ], + "credits": 17.0, + "term": 7 + }, + { + "courses": [ + "AWA201", + "CS201", + "DA101", + "FE03", + "FE07S" + ], + "credits": 14.0, + "term": 8 + } + ], + "terms_required": 8, + "total_complexity": 179 + }, + { + "category": "Random Sample", + "course_count": 37, + "credits": 122.0, + "critical_path": [ + "CS150B", + "CS164", + "CS165", + "CS253", + "CS314", + "CS453" + ], + "is_calc_ready": false, + "longest_delay": 6, + "schedule": [ + { + "courses": [ + "CS150B", + "MATH156", + "STAT302A", + "CS201", + "FE03", + "SBA301" + ], + "credits": 17.0, + "term": 1 + }, + { + "courses": [ + "CS164", + "DSCI369", + "DA101", + "FE04" + ], + "credits": 14.0, + "term": 2 + }, + { + "courses": [ + "CS165", + "CS220", + "CS250", + "CT301" + ], + "credits": 14.0, + "term": 3 + }, + { + "courses": [ + "CS253", + "CS345", + "CS370", + "CS320", + "FE07S" + ], + "credits": 15.0, + "term": 4 + }, + { + "courses": [ + "CS314", + "CS310H", + "CS356", + "CS425", + "HA301" + ], + "credits": 16.0, + "term": 5 + }, + { + "courses": [ + "CS453", + "CS430", + "CS464", + "FE06" + ], + "credits": 15.0, + "term": 6 + }, + { + "courses": [ + "CS475", + "MATH256", + "FE01", + "FE05" + ], + "credits": 14.0, + "term": 7 + }, + { + "courses": [ + "AHA301", + "AWA201", + "CHEM111", + "FE02", + "LIFE102" + ], + "credits": 17.0, + "term": 8 + } + ], + "terms_required": 8, + "total_complexity": 200 + }, + { + "category": "Random Sample", + "course_count": 37, + "credits": 122.0, + "critical_path": [ + "MATH160", + "CS152", + "CS164", + "CS165", + "CS253", + "CS314", + "CS415" + ], + "is_calc_ready": false, + "longest_delay": 7, + "schedule": [ + { + "courses": [ + "MATH160", + "BZ350", + "DA101", + "FE05" + ], + "credits": 14.0, + "term": 1 + }, + { + "courses": [ + "CS152", + "DSCI369", + "MATH161", + "FE02", + "LIFE102" + ], + "credits": 17.0, + "term": 2 + }, + { + "courses": [ + "CS164", + "CS162", + "CHEM111", + "FE03", + "MATH156" + ], + "credits": 17.0, + "term": 3 + }, + { + "courses": [ + "CS165", + "CS270", + "CS220", + "FE06" + ], + "credits": 15.0, + "term": 4 + }, + { + "courses": [ + "CS253", + "CS370", + "CS320", + "FE04", + "SBA301" + ], + "credits": 16.0, + "term": 5 + }, + { + "courses": [ + "CS314", + "CS310H", + "CS356", + "CS410", + "STAT302A" + ], + "credits": 14.0, + "term": 6 + }, + { + "courses": [ + "CS415", + "CS464", + "CS470", + "FE07S" + ], + "credits": 14.0, + "term": 7 + }, + { + "courses": [ + "AHA301", + "AWA201", + "CS201", + "FE01", + "HA301" + ], + "credits": 15.0, + "term": 8 + } + ], + "terms_required": 8, + "total_complexity": 198 + } + ], + "courses": { + "BZ350": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "BZ350", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 435 + }, + "name": "Molecular and General Genetics", + "number": "350", + "prefix": "BZ", + "strict_corequisites": [] + }, + "BZ360": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "BZ360", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 423 + }, + "name": "Bioinformatics and Genomics", + "number": "360", + "prefix": "BZ", + "strict_corequisites": [] + }, + "CHEM111": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "gen_ed_attributes": [ + "AUCC-3A" + ], + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "CHEM111", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 10000 + }, + "name": "General Chemistry I", + "number": "111", + "prefix": "CHEM", + "strict_corequisites": [] + }, + "CHEM112": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "gen_ed_attributes": [ + "AUCC-3A" + ], + "id": null, + "name": "General Chemistry II", + "number": "112", + "prefix": "CHEM", + "prerequisites": "CHEM111", + "strict_corequisites": [] + }, + "CIS320": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "CIS320", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 412 + }, + "name": "Project Management for Information Systems", + "number": "320", + "prefix": "CIS", + "strict_corequisites": [] + }, + "CO150": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "gen_ed_attributes": [ + "AUCC-1A", + "GT-CO2" + ], + "id": null, + "name": "College Composition (GT-CO2)", + "number": "150", + "prefix": "CO", + "strict_corequisites": [] + }, + "CS150A": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 21.0, + "mean": 13.699907235621508, + "median": 13.699907235621508, + "min": 0.0, + "q1": 7.307409604380586, + "q3": 20.09240486686243, + "std_dev": 6.392497631240921 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 27.0, + "mean": 18.627782931354286, + "median": 22.0, + "min": 1.0, + "q1": 21.0, + "q3": 23.0, + "std_dev": 8.18721671470751 + }, + "course_id": "CS150A", + "delay": { + "max": 6.0, + "mean": 4.927875695732843, + "median": 4.927875695732843, + "min": 1.0, + "q1": 3.0810908107869563, + "q3": 6.0, + "std_dev": 1.8467848849458868 + }, + "plan_count": 4312 + }, + "name": "Culture and Coding - Java", + "number": "150A", + "prefix": "CS", + "strict_corequisites": [] + }, + "CS150B": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 21.0, + "mean": 16.601167315175072, + "median": 16.601167315175072, + "min": 14.0, + "q1": 15.400061296099505, + "q3": 17.80227333425064, + "std_dev": 1.2011060190755665 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 27.0, + "mean": 22.372707059477463, + "median": 22.0, + "min": 19.0, + "q1": 21.0, + "q3": 23.0, + "std_dev": 1.2754578065316582 + }, + "course_id": "CS150B", + "delay": { + "max": 6.0, + "mean": 5.771539744302377, + "median": 5.771539744302377, + "min": 5.0, + "q1": 5.351699099218802, + "q3": 6.0, + "std_dev": 0.41984064508357466 + }, + "plan_count": 3598 + }, + "name": "Culture and Coding - Python", + "number": "150B", + "prefix": "CS", + "strict_corequisites": [] + }, + "CS152": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 2.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 21.0, + "mean": 16.63476425052782, + "median": 16.63476425052782, + "min": 14.0, + "q1": 15.426814205298662, + "q3": 17.842714295756977, + "std_dev": 1.2079500452291585 + }, + "centrality": { + "max": 133.0, + "mean": 79.798733286418, + "median": 79.798733286418, + "min": 48.0, + "q1": 66.89726514636371, + "q3": 92.70020142647229, + "std_dev": 12.90146814005429 + }, + "complexity": { + "max": 28.0, + "mean": 23.397607318789593, + "median": 23.0, + "min": 20.0, + "q1": 22.0, + "q3": 24.0, + "std_dev": 1.303746527111612 + }, + "course_id": "CS152", + "delay": { + "max": 7.0, + "mean": 6.762843068261781, + "median": 6.762843068261781, + "min": 6.0, + "q1": 6.337503766683936, + "q3": 7.0, + "std_dev": 0.42533930157784594 + }, + "plan_count": 2842 + }, + "name": "Python for STEM", + "number": "152", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS163", + "MATH156", + "MATH160" + ] + }, + "strict_corequisites": [] + }, + "CS162": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 2.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 19.0, + "mean": 7.547629899726518, + "median": 7.547629899726518, + "min": 0.0, + "q1": 0.0, + "q3": 15.27352081682103, + "std_dev": 7.725890917094511 + }, + "centrality": { + "max": 133.0, + "mean": 34.60095715587969, + "median": 34.60095715587969, + "min": 0.0, + "q1": 0.0, + "q3": 71.04752609474784, + "std_dev": 36.44656893886814 + }, + "complexity": { + "max": 25.0, + "mean": 12.071330902461233, + "median": 18.0, + "min": 3.0, + "q1": 3.0, + "q3": 22.0, + "std_dev": 9.282665761701553 + }, + "course_id": "CS162", + "delay": { + "max": 7.0, + "mean": 4.523701002734738, + "median": 4.523701002734738, + "min": 3.0, + "q1": 3.0, + "q3": 6.137687775756851, + "std_dev": 1.6139867730221134 + }, + "plan_count": 4388 + }, + "name": "CS1 - Introduction to Java Programming", + "number": "162", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS150B", + "CS152", + "CS163" + ] + }, + "strict_corequisites": [] + }, + "CS163": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 20.0, + "mean": 15.903073286052013, + "median": 15.903073286052013, + "min": 13.0, + "q1": 14.653530035072736, + "q3": 17.15261653703129, + "std_dev": 1.249543250979277 + }, + "centrality": { + "max": 116.0, + "mean": 67.1283350219519, + "median": 67.1283350219519, + "min": 40.0, + "q1": 56.34989906119671, + "q3": 77.90677098270709, + "std_dev": 10.778435960755191 + }, + "complexity": { + "max": 26.0, + "mean": 21.659912191827104, + "median": 22.0, + "min": 18.0, + "q1": 21.0, + "q3": 23.0, + "std_dev": 1.3359310531791364 + }, + "course_id": "CS163", + "delay": { + "max": 6.0, + "mean": 5.756838905775087, + "median": 5.756838905775087, + "min": 5.0, + "q1": 5.327847324878373, + "q3": 6.0, + "std_dev": 0.4289915808967139 + }, + "plan_count": 2961 + }, + "name": "CS1 - No Prior Programming Experience", + "number": "163", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS150A", + "CS150B" + ] + }, + "strict_corequisites": [] + }, + "CS164": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 19.0, + "mean": 15.350460593654024, + "median": 15.350460593654024, + "min": 12.0, + "q1": 14.173401522040187, + "q3": 16.52751966526786, + "std_dev": 1.177059071613837 + }, + "centrality": { + "max": 125.0, + "mean": 70.75557830092137, + "median": 70.75557830092137, + "min": 39.0, + "q1": 57.30576514235794, + "q3": 84.2053914594848, + "std_dev": 13.449813158563426 + }, + "complexity": { + "max": 26.0, + "mean": 21.550870010235418, + "median": 21.0, + "min": 18.0, + "q1": 21.0, + "q3": 22.0, + "std_dev": 1.3400842334740874 + }, + "course_id": "CS164", + "delay": { + "max": 7.0, + "mean": 6.200409416581367, + "median": 6.200409416581367, + "min": 5.0, + "q1": 5.552793652153821, + "q3": 6.848025181008913, + "std_dev": 0.6476157644275461 + }, + "plan_count": 4885 + }, + "name": "CS1 - Computational Thinking with Java", + "number": "164", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS150A", + "CS150B", + "CS152", + "CS163" + ] + }, + "strict_corequisites": [] + }, + "CS165": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 15.0, + "mean": 11.641499999999995, + "median": 11.641499999999995, + "min": 9.0, + "q1": 10.666421669813136, + "q3": 12.616578330186854, + "std_dev": 0.9750783301868595 + }, + "centrality": { + "max": 77.0, + "mean": 45.9818000000001, + "median": 45.9818000000001, + "min": 26.0, + "q1": 38.40661229539502, + "q3": 53.55698770460518, + "std_dev": 7.575187704605074 + }, + "complexity": { + "max": 22.0, + "mean": 17.689800000000055, + "median": 18.0, + "min": 14.0, + "q1": 17.0, + "q3": 19.0, + "std_dev": 1.17931164668208 + }, + "course_id": "CS165", + "delay": { + "max": 7.0, + "mean": 6.0483, + "median": 6.0483, + "min": 5.0, + "q1": 5.429456635326841, + "q3": 6.6671433646731595, + "std_dev": 0.6188433646731596 + }, + "plan_count": 10000 + }, + "name": "CS2 - Data Structures", + "number": "165", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS162", + "CS163", + "CS164" + ] + }, + "strict_corequisites": [] + }, + "CS201": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "cross_listed_as": [ + "PHIL201" + ], + "csv_id": null, + "gen_ed_attributes": [ + "AUCC-3B", + "GT-AH3" + ], + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "CS201", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 10000 + }, + "name": "Ethical Computing Systems (GT-AH3)", + "number": "201", + "prefix": "CS", + "strict_corequisites": [] + }, + "CS214": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 9.0, + "mean": 3.834452652787096, + "median": 3.834452652787096, + "min": 0.0, + "q1": 1.579960446084673, + "q3": 6.088944859489519, + "std_dev": 2.254492206702423 + }, + "centrality": { + "max": 45.0, + "mean": 17.601242444593748, + "median": 17.601242444593748, + "min": 0.0, + "q1": 7.097142132221338, + "q3": 28.10534275696616, + "std_dev": 10.50410031237241 + }, + "complexity": { + "max": 15.0, + "mean": 9.56161853593019, + "median": 10.5, + "min": 4.0, + "q1": 8.0, + "q3": 12.0, + "std_dev": 2.9117990786853634 + }, + "course_id": "CS214", + "delay": { + "max": 7.0, + "mean": 5.72716588314304, + "median": 5.72716588314304, + "min": 4.0, + "q1": 4.869062980794661, + "q3": 6.585268785491419, + "std_dev": 0.8581029023483784 + }, + "plan_count": 5956 + }, + "name": "Software Development", + "number": "214", + "prefix": "CS", + "prerequisites": "CS165", + "strict_corequisites": [] + }, + "CS220": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 8.0, + "mean": 3.3390000000000017, + "median": 3.3390000000000017, + "min": 1.0, + "q1": 2.136719250757132, + "q3": 4.541280749242871, + "std_dev": 1.2022807492428695 + }, + "centrality": { + "max": 48.0, + "mean": 12.388100000000012, + "median": 12.388100000000012, + "min": 4.0, + "q1": 6.432541387241677, + "q3": 18.343658612758347, + "std_dev": 5.9555586127583355 + }, + "complexity": { + "max": 14.0, + "mean": 8.460399999999963, + "median": 9.0, + "min": 5.0, + "q1": 8.0, + "q3": 9.0, + "std_dev": 1.5101760956921533 + }, + "course_id": "CS220", + "delay": { + "max": 6.0, + "mean": 5.121400000000004, + "median": 5.121400000000004, + "min": 4.0, + "q1": 4.554423774748892, + "q3": 5.688376225251116, + "std_dev": 0.5669762252511125 + }, + "plan_count": 10000 + }, + "name": "Discrete Structures and the Applications", + "number": "220", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS152", + "CS162", + "CS163", + "CS164" + ] + }, + "strict_corequisites": [] + }, + "CS250": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 8.0, + "mean": 2.3466319165998377, + "median": 2.3466319165998377, + "min": 1.0, + "q1": 1.2081040632494573, + "q3": 3.485159769950218, + "std_dev": 1.1385278533503804 + }, + "centrality": { + "max": 36.0, + "mean": 8.240978348035286, + "median": 8.240978348035286, + "min": 4.0, + "q1": 4.0, + "q3": 12.798776639510042, + "std_dev": 4.557798291474755 + }, + "complexity": { + "max": 15.0, + "mean": 7.313953488372089, + "median": 7.0, + "min": 5.0, + "q1": 6.0, + "q3": 8.0, + "std_dev": 1.630721304262345 + }, + "course_id": "CS250", + "delay": { + "max": 7.0, + "mean": 4.967321571772249, + "median": 4.967321571772249, + "min": 4.0, + "q1": 4.2777740206778345, + "q3": 5.656869122866664, + "std_dev": 0.6895475510944146 + }, + "plan_count": 4988 + }, + "name": "Computer Systems Foundations", + "number": "250", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS162", + "CS163", + "CS164" + ] + }, + "strict_corequisites": [] + }, + "CS253": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 9.0, + "mean": 4.23562845079572, + "median": 4.23562845079572, + "min": 0.0, + "q1": 2.141113462461079, + "q3": 6.330143439130361, + "std_dev": 2.094514988334641 + }, + "centrality": { + "max": 46.0, + "mean": 19.410360506658044, + "median": 19.410360506658044, + "min": 0.0, + "q1": 9.51857825460332, + "q3": 29.30214275871277, + "std_dev": 9.891782252054725 + }, + "complexity": { + "max": 16.0, + "mean": 10.043520623579061, + "median": 11.0, + "min": 4.0, + "q1": 9.0, + "q3": 12.0, + "std_dev": 2.7365169326208183 + }, + "course_id": "CS253", + "delay": { + "max": 7.0, + "mean": 5.80789217278338, + "median": 5.80789217278338, + "min": 4.0, + "q1": 4.962818140384867, + "q3": 6.652966205181894, + "std_dev": 0.8450740323985136 + }, + "plan_count": 6158 + }, + "name": "Software Development with C++", + "number": "253", + "prefix": "CS", + "prerequisites": "CS165", + "strict_corequisites": [] + }, + "CS270": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 7.0, + "mean": 2.353152434158026, + "median": 2.353152434158026, + "min": 1.0, + "q1": 1.2347687070225826, + "q3": 3.4715361612934696, + "std_dev": 1.1183837271354435 + }, + "centrality": { + "max": 30.0, + "mean": 8.218475658419802, + "median": 8.218475658419802, + "min": 4.0, + "q1": 4.0, + "q3": 12.711437458506364, + "std_dev": 4.4929618000865625 + }, + "complexity": { + "max": 13.0, + "mean": 7.323822825219455, + "median": 7.0, + "min": 5.0, + "q1": 6.0, + "q3": 8.0, + "std_dev": 1.581986307560286 + }, + "course_id": "CS270", + "delay": { + "max": 7.0, + "mean": 4.970670391061463, + "median": 4.970670391061463, + "min": 4.0, + "q1": 4.2952129705288895, + "q3": 5.646127811594037, + "std_dev": 0.6754574205325737 + }, + "plan_count": 5012 + }, + "name": "Computer Organization", + "number": "270", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS163", + "CS164" + ] + }, + "strict_corequisites": [] + }, + "CS310H": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "cross_listed_as": [ + "IDEA310H" + ], + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 5.284086863652547, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 6.0, + "std_dev": 0.450978400317139 + }, + "course_id": "CS310H", + "delay": { + "max": 6.0, + "mean": 5.284086863652547, + "median": 5.284086863652547, + "min": 5.0, + "q1": 5.0, + "q3": 5.735065263969686, + "std_dev": 0.450978400317139 + }, + "plan_count": 5618 + }, + "name": "Design Thinking Toolbox: Mixed Reality Design", + "number": "310H", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS214", + "CS253", + "IDEA210" + ] + }, + "strict_corequisites": [] + }, + "CS312": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 4.280474649406691, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 5.0, + "std_dev": 0.4492311436742593 + }, + "course_id": "CS312", + "delay": { + "max": 5.0, + "mean": 4.280474649406691, + "median": 4.280474649406691, + "min": 4.0, + "q1": 4.0, + "q3": 4.72970579308095, + "std_dev": 0.4492311436742593 + }, + "plan_count": 5562 + }, + "name": "Modern Web Applications", + "number": "312", + "prefix": "CS", + "prerequisites": "CS165", + "strict_corequisites": [] + }, + "CS314": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 4.0, + "mean": 0.9726000000000005, + "median": 0.9726000000000005, + "min": 0.0, + "q1": 0.10770160134267648, + "q3": 1.8374983986573246, + "std_dev": 0.864898398657324 + }, + "centrality": { + "max": 28.0, + "mean": 6.108499999999987, + "median": 6.108499999999987, + "min": 0.0, + "q1": 0.6483967271671336, + "q3": 11.56860327283284, + "std_dev": 5.460103272832853 + }, + "complexity": { + "max": 11.0, + "mean": 6.930300000000017, + "median": 7.0, + "min": 5.0, + "q1": 6.0, + "q3": 8.0, + "std_dev": 1.3402394972541323 + }, + "course_id": "CS314", + "delay": { + "max": 7.0, + "mean": 5.957699999999997, + "median": 5.957699999999997, + "min": 5.0, + "q1": 5.309540521167819, + "q3": 6.605859478832175, + "std_dev": 0.6481594788321784 + }, + "plan_count": 10000 + }, + "name": "Software Engineering", + "number": "314", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS214", + "CS253" + ] + }, + "strict_corequisites": [] + }, + "CS320": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 5.0, + "mean": 1.324599999999998, + "median": 1.324599999999998, + "min": 0.0, + "q1": 0.3503131633856488, + "q3": 2.298886836614347, + "std_dev": 0.9742868366143492 + }, + "centrality": { + "max": 95.0, + "mean": 23.254500000000075, + "median": 23.254500000000075, + "min": 0.0, + "q1": 6.081226294908177, + "q3": 40.42777370509197, + "std_dev": 17.173273705091898 + }, + "complexity": { + "max": 11.0, + "mean": 6.406299999999996, + "median": 6.0, + "min": 4.0, + "q1": 6.0, + "q3": 7.0, + "std_dev": 1.3648517538546072 + }, + "course_id": "CS320", + "delay": { + "max": 6.0, + "mean": 5.081700000000013, + "median": 5.081700000000013, + "min": 4.0, + "q1": 4.473896487341522, + "q3": 5.689503512658504, + "std_dev": 0.6078035126584911 + }, + "plan_count": 10000 + }, + "name": "Algorithms - Theory and Practice", + "number": "320", + "prefix": "CS", + "prerequisites": { + "and": [ + "CS220", + "CS165", + { + "or": [ + "MATH156", + "MATH160" + ] + }, + { + "or": [ + "DSCI369", + "MATH369" + ] + } + ] + }, + "strict_corequisites": [] + }, + "CS345": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 3.0, + "mean": 0.8393471885757992, + "median": 0.8393471885757992, + "min": 0.0, + "q1": 0.1740398370926951, + "q3": 1.5046545400589033, + "std_dev": 0.665307351483104 + }, + "centrality": { + "max": 57.0, + "mean": 14.038888180543195, + "median": 14.038888180543195, + "min": 0.0, + "q1": 2.891314007968564, + "q3": 25.186462353117825, + "std_dev": 11.14757417257463 + }, + "complexity": { + "max": 9.0, + "mean": 5.817544307025383, + "median": 6.0, + "min": 4.0, + "q1": 5.0, + "q3": 7.0, + "std_dev": 1.1741133365926704 + }, + "course_id": "CS345", + "delay": { + "max": 6.0, + "mean": 4.978197118449554, + "median": 4.978197118449554, + "min": 4.0, + "q1": 4.330003863795768, + "q3": 5.62639037310334, + "std_dev": 0.6481932546537861 + }, + "plan_count": 7843 + }, + "name": "Machine Learning Foundations and Practice", + "number": "345", + "prefix": "CS", + "prerequisites": { + "and": [ + "CS220", + { + "or": [ + "CS150B", + "CS152", + "CS165", + "DSCI235" + ] + }, + { + "or": [ + "MATH156", + "MATH160" + ] + }, + { + "or": [ + "STAT301", + "STAT302A", + "STAT307", + "STAT315" + ] + } + ] + }, + "strict_corequisites": [], + "tags": [ + "ai" + ] + }, + "CS356": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 1.0, + "mean": 0.3492599742599733, + "median": 0.3492599742599733, + "min": 0.0, + "q1": 0.0, + "q3": 0.8259962164735242, + "std_dev": 0.4767362422135508 + }, + "centrality": { + "max": 14.0, + "mean": 2.4527027027027115, + "median": 2.4527027027027115, + "min": 0.0, + "q1": 0.0, + "q3": 6.017151036343888, + "std_dev": 3.5644483336411765 + }, + "complexity": { + "max": 8.0, + "mean": 5.984073359073361, + "median": 6.0, + "min": 5.0, + "q1": 5.0, + "q3": 7.0, + "std_dev": 1.0608439294977823 + }, + "course_id": "CS356", + "delay": { + "max": 7.0, + "mean": 5.634813384813354, + "median": 5.634813384813354, + "min": 5.0, + "q1": 5.0, + "q3": 6.296193613481867, + "std_dev": 0.6613802286685124 + }, + "plan_count": 6216 + }, + "name": "Systems Security", + "number": "356", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS214", + "CS253", + "CS370" + ] + }, + "strict_corequisites": [] + }, + "CS370": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 6.0, + "mean": 1.094500000000005, + "median": 1.094500000000005, + "min": 0.0, + "q1": 0.01775966454303024, + "q3": 2.1712403354569796, + "std_dev": 1.0767403354569747 + }, + "centrality": { + "max": 62.0, + "mean": 11.354399999999918, + "median": 11.354399999999918, + "min": 0.0, + "q1": 0.35807331150977184, + "q3": 22.350726688490063, + "std_dev": 10.996326688490146 + }, + "complexity": { + "max": 13.0, + "mean": 6.063499999999986, + "median": 6.0, + "min": 4.0, + "q1": 5.0, + "q3": 7.0, + "std_dev": 1.585013485747046 + }, + "course_id": "CS370", + "delay": { + "max": 7.0, + "mean": 4.968999999999993, + "median": 4.968999999999993, + "min": 4.0, + "q1": 4.286476007747707, + "q3": 5.6515239922522795, + "std_dev": 0.6825239922522862 + }, + "plan_count": 10000 + }, + "name": "Operating Systems", + "number": "370", + "prefix": "CS", + "prerequisites": { + "and": [ + "CS165", + { + "or": [ + "CS250", + "CS270" + ] + } + ] + }, + "strict_corequisites": [] + }, + "CS410": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 5.269478495740697, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 6.0, + "std_dev": 0.4436888955946841 + }, + "course_id": "CS410", + "delay": { + "max": 6.0, + "mean": 5.269478495740697, + "median": 5.269478495740697, + "min": 5.0, + "q1": 5.0, + "q3": 5.713167391335381, + "std_dev": 0.4436888955946841 + }, + "plan_count": 4813 + }, + "name": "Introduction to Computer Graphics", + "number": "410", + "prefix": "CS", + "prerequisites": { + "and": [ + { + "or": [ + "CS214", + "CS253" + ] + }, + { + "or": [ + "DSCI369", + "MATH369" + ] + } + ] + }, + "strict_corequisites": [] + }, + "CS414": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 7.0, + "mean": 6.272393048128343, + "median": 6.0, + "min": 6.0, + "q1": 6.0, + "q3": 7.0, + "std_dev": 0.4451910550086252 + }, + "course_id": "CS414", + "delay": { + "max": 7.0, + "mean": 6.272393048128343, + "median": 6.272393048128343, + "min": 6.0, + "q1": 6.0, + "q3": 6.717584103136969, + "std_dev": 0.4451910550086252 + }, + "plan_count": 2992 + }, + "name": "Object Oriented Design", + "number": "414", + "prefix": "CS", + "prerequisites": "CS314", + "strict_corequisites": [] + }, + "CS415": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 7.0, + "mean": 6.282384823848234, + "median": 6.0, + "min": 6.0, + "q1": 6.0, + "q3": 7.0, + "std_dev": 0.45015956627449083 + }, + "course_id": "CS415", + "delay": { + "max": 7.0, + "mean": 6.282384823848234, + "median": 6.282384823848234, + "min": 6.0, + "q1": 6.0, + "q3": 6.732544390122724, + "std_dev": 0.45015956627449083 + }, + "plan_count": 1845 + }, + "name": "Software Testing", + "number": "415", + "prefix": "CS", + "prerequisites": "CS314", + "strict_corequisites": [] + }, + "CS420": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 5.2796571944068615, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 6.0, + "std_dev": 0.44883075654787924 + }, + "course_id": "CS420", + "delay": { + "max": 6.0, + "mean": 5.2796571944068615, + "median": 5.2796571944068615, + "min": 5.0, + "q1": 5.0, + "q3": 5.728487950954741, + "std_dev": 0.44883075654787924 + }, + "plan_count": 2217 + }, + "name": "Introduction to Analysis of Algorithms", + "number": "420", + "prefix": "CS", + "prerequisites": "CS320", + "strict_corequisites": [] + }, + "CS422": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 5.293535235876531, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 6.0, + "std_dev": 0.45538148971542475 + }, + "course_id": "CS422", + "delay": { + "max": 6.0, + "mean": 5.293535235876531, + "median": 5.293535235876531, + "min": 5.0, + "q1": 5.0, + "q3": 5.748916725591956, + "std_dev": 0.45538148971542475 + }, + "plan_count": 1717 + }, + "name": "Automata, Logic, and Computation", + "number": "422", + "prefix": "CS", + "prerequisites": "CS320", + "strict_corequisites": [] + }, + "CS425": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 5.287769784172655, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 6.0, + "std_dev": 0.45272324381445384 + }, + "course_id": "CS425", + "delay": { + "max": 6.0, + "mean": 5.287769784172655, + "median": 5.287769784172655, + "min": 5.0, + "q1": 5.0, + "q3": 5.7404930279871085, + "std_dev": 0.45272324381445384 + }, + "plan_count": 2224 + }, + "name": "Introduction to Bioinformatics Algorithms", + "number": "425", + "prefix": "CS", + "prerequisites": { + "and": [ + { + "or": [ + "BZ360", + "CS320" + ] + }, + "CS345" + ] + }, + "strict_corequisites": [] + }, + "CS430": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 7.0, + "mean": 5.499320959710276, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 6.0, + "std_dev": 0.610103918242277 + }, + "course_id": "CS430", + "delay": { + "max": 7.0, + "mean": 5.499320959710276, + "median": 5.499320959710276, + "min": 5.0, + "q1": 5.0, + "q3": 6.109424877952553, + "std_dev": 0.610103918242277 + }, + "plan_count": 2209 + }, + "name": "Database Systems", + "number": "430", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS314", + "CS370" + ] + }, + "strict_corequisites": [] + }, + "CS435": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 5.292717086834747, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 6.0, + "std_dev": 0.455009663534436 + }, + "course_id": "CS435", + "delay": { + "max": 6.0, + "mean": 5.292717086834747, + "median": 5.292717086834747, + "min": 5.0, + "q1": 5.0, + "q3": 5.747726750369183, + "std_dev": 0.455009663534436 + }, + "plan_count": 2142 + }, + "name": "Introduction to Big Data", + "number": "435", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS320", + "CS370" + ] + }, + "strict_corequisites": [] + }, + "CS440": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 5.290925672594626, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 6.0, + "std_dev": 0.45418930592869294 + }, + "course_id": "CS440", + "delay": { + "max": 6.0, + "mean": 5.290925672594626, + "median": 5.290925672594626, + "min": 5.0, + "q1": 5.0, + "q3": 5.745114978523318, + "std_dev": 0.45418930592869294 + }, + "plan_count": 2193 + }, + "name": "Introduction to Artificial Intelligence", + "number": "440", + "prefix": "CS", + "prerequisites": { + "and": [ + "CS320", + "CS345" + ] + }, + "strict_corequisites": [], + "tags": [ + "ai" + ] + }, + "CS445": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 5.277469990766387, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 6.0, + "std_dev": 0.44775037129017503 + }, + "course_id": "CS445", + "delay": { + "max": 6.0, + "mean": 5.277469990766387, + "median": 5.277469990766387, + "min": 5.0, + "q1": 5.0, + "q3": 5.725220362056562, + "std_dev": 0.44775037129017503 + }, + "plan_count": 2166 + }, + "name": "Introduction to Machine Learning", + "number": "445", + "prefix": "CS", + "prerequisites": { + "and": [ + "CS165", + { + "or": [ + "CS345", + "DSCI235" + ] + }, + { + "or": [ + "DSCI369", + "MATH369" + ] + } + ] + }, + "strict_corequisites": [], + "tags": [ + "ai" + ] + }, + "CS453": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 7.0, + "mean": 6.286039108685772, + "median": 6.0, + "min": 6.0, + "q1": 6.0, + "q3": 7.0, + "std_dev": 0.4519078855121019 + }, + "course_id": "CS453", + "delay": { + "max": 7.0, + "mean": 6.286039108685772, + "median": 6.286039108685772, + "min": 6.0, + "q1": 6.0, + "q3": 6.737946994197874, + "std_dev": 0.4519078855121019 + }, + "plan_count": 2199 + }, + "name": "Introduction to Compiler Construction", + "number": "453", + "prefix": "CS", + "prerequisites": "CS314", + "strict_corequisites": [] + }, + "CS454": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 5.29229366165071, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 6.0, + "std_dev": 0.4548165311524298 + }, + "course_id": "CS454", + "delay": { + "max": 6.0, + "mean": 5.29229366165071, + "median": 5.29229366165071, + "min": 5.0, + "q1": 5.0, + "q3": 5.74711019280314, + "std_dev": 0.4548165311524298 + }, + "plan_count": 2193 + }, + "name": "Principles of Programming Languages", + "number": "454", + "prefix": "CS", + "prerequisites": { + "and": [ + "CS253", + "CS320" + ] + }, + "strict_corequisites": [] + }, + "CS455": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 5.2749999999999995, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 6.0, + "std_dev": 0.4465142774872939 + }, + "course_id": "CS455", + "delay": { + "max": 6.0, + "mean": 5.2749999999999995, + "median": 5.2749999999999995, + "min": 5.0, + "q1": 5.0, + "q3": 5.721514277487294, + "std_dev": 0.4465142774872939 + }, + "plan_count": 2160 + }, + "name": "Introduction to Distributed Systems", + "number": "455", + "prefix": "CS", + "prerequisites": "CS370", + "strict_corequisites": [] + }, + "CS456": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 7.0, + "mean": 6.294334408106863, + "median": 6.0, + "min": 6.0, + "q1": 6.0, + "q3": 7.0, + "std_dev": 0.4557429805397398 + }, + "course_id": "CS456", + "delay": { + "max": 7.0, + "mean": 6.294334408106863, + "median": 6.294334408106863, + "min": 6.0, + "q1": 6.0, + "q3": 6.750077388646603, + "std_dev": 0.4557429805397398 + }, + "plan_count": 2171 + }, + "name": "Modern CyberSecurity", + "number": "456", + "prefix": "CS", + "prerequisites": "CS356", + "strict_corequisites": [] + }, + "CS457": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 5.283234353586111, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 6.0, + "std_dev": 0.45056925609141274 + }, + "course_id": "CS457", + "delay": { + "max": 6.0, + "mean": 5.283234353586111, + "median": 5.283234353586111, + "min": 5.0, + "q1": 5.0, + "q3": 5.733803609677524, + "std_dev": 0.45056925609141274 + }, + "plan_count": 2189 + }, + "name": "Computer Networks and the Internet", + "number": "457", + "prefix": "CS", + "prerequisites": { + "and": [ + { + "or": [ + "CS214", + "CS253" + ] + }, + "CS370", + { + "or": [ + "STAT301", + "STAT307", + "STAT315" + ] + } + ] + }, + "strict_corequisites": [] + }, + "CS458": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 7.0, + "mean": 6.281557743957028, + "median": 6.0, + "min": 6.0, + "q1": 6.0, + "q3": 7.0, + "std_dev": 0.4497588028875657 + }, + "course_id": "CS458", + "delay": { + "max": 7.0, + "mean": 6.281557743957028, + "median": 6.281557743957028, + "min": 6.0, + "q1": 6.0, + "q3": 6.731316546844593, + "std_dev": 0.4497588028875657 + }, + "plan_count": 2234 + }, + "name": "Blockchain Principles and Applications", + "number": "458", + "prefix": "CS", + "prerequisites": "CS314", + "strict_corequisites": [] + }, + "CS462": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 5.275115207373265, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 6.0, + "std_dev": 0.44657231222416127 + }, + "course_id": "CS462", + "delay": { + "max": 6.0, + "mean": 5.275115207373265, + "median": 5.275115207373265, + "min": 5.0, + "q1": 5.0, + "q3": 5.721687519597426, + "std_dev": 0.44657231222416127 + }, + "plan_count": 2170 + }, + "name": "Engaging in Virtual Worlds", + "number": "462", + "prefix": "CS", + "prerequisites": { + "and": [ + { + "or": [ + "CS214", + "CS253" + ] + }, + { + "or": [ + "DSCI369", + "MATH369" + ] + } + ] + }, + "strict_corequisites": [] + }, + "CS464": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 5.295006871278046, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 6.0, + "std_dev": 0.4560458498624773 + }, + "course_id": "CS464", + "delay": { + "max": 6.0, + "mean": 5.295006871278046, + "median": 5.295006871278046, + "min": 5.0, + "q1": 5.0, + "q3": 5.751052721140523, + "std_dev": 0.4560458498624773 + }, + "plan_count": 2183 + }, + "name": "Principles of Human Computer Interaction", + "number": "464", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS214", + "CS253" + ] + }, + "strict_corequisites": [] + }, + "CS465": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 5.292775665399233, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 6.0, + "std_dev": 0.45503634486628824 + }, + "course_id": "CS465", + "delay": { + "max": 6.0, + "mean": 5.292775665399233, + "median": 5.292775665399233, + "min": 5.0, + "q1": 5.0, + "q3": 5.7478120102655215, + "std_dev": 0.45503634486628824 + }, + "plan_count": 2104 + }, + "name": "Multimodal Interaction for 3D User Interfaces", + "number": "465", + "prefix": "CS", + "prerequisites": { + "and": [ + "CS214", + "CT301" + ] + }, + "strict_corequisites": [] + }, + "CS470": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 5.28748280605226, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 6.0, + "std_dev": 0.4525886015760705 + }, + "course_id": "CS470", + "delay": { + "max": 6.0, + "mean": 5.28748280605226, + "median": 5.28748280605226, + "min": 5.0, + "q1": 5.0, + "q3": 5.740071407628331, + "std_dev": 0.4525886015760705 + }, + "plan_count": 2181 + }, + "name": "Computer Architecture", + "number": "470", + "prefix": "CS", + "prerequisites": "CS370", + "strict_corequisites": [] + }, + "CS475": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 5.272942289498577, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 6.0, + "std_dev": 0.44547143129706307 + }, + "course_id": "CS475", + "delay": { + "max": 6.0, + "mean": 5.272942289498577, + "median": 5.272942289498577, + "min": 5.0, + "q1": 5.0, + "q3": 5.71841372079564, + "std_dev": 0.44547143129706307 + }, + "plan_count": 2114 + }, + "name": "Parallel Programming", + "number": "475", + "prefix": "CS", + "prerequisites": { + "and": [ + { + "or": [ + "CS250", + "CS270" + ] + }, + "CS320", + "CT301" + ] + }, + "strict_corequisites": [] + }, + "CT301": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 2.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 2.0, + "mean": 0.6151378153711544, + "median": 0.6151378153711544, + "min": 0.0, + "q1": 0.02607450774250042, + "q3": 1.2042011229998084, + "std_dev": 0.589063307628654 + }, + "centrality": { + "max": 10.0, + "mean": 2.6345340527927714, + "median": 2.6345340527927714, + "min": 0.0, + "q1": 0.08417664043440132, + "q3": 5.184891465151141, + "std_dev": 2.55035741235837 + }, + "complexity": { + "max": 7.0, + "mean": 4.460113752369836, + "median": 5.0, + "min": 3.0, + "q1": 3.0, + "q3": 5.0, + "std_dev": 1.155121945994071 + }, + "course_id": "CT301", + "delay": { + "max": 5.0, + "mean": 3.8449759369986847, + "median": 3.8449759369986847, + "min": 3.0, + "q1": 3.1751825209238183, + "q3": 4.514769353073551, + "std_dev": 0.6697934160748662 + }, + "plan_count": 6857 + }, + "name": "C++ Fundamentals", + "number": "301", + "prefix": "CT", + "prerequisites": { + "or": [ + "CS162", + "CS163", + "CS164" + ] + }, + "strict_corequisites": [] + }, + "DSCI235": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 2.0, + "csv_id": null, + "id": null, + "name": "Data Wrangling", + "number": "235", + "prefix": "DSCI", + "prerequisites": { + "and": [ + { + "or": [ + "CS150B", + "CS152" + ] + }, + "CS165" + ] + }, + "strict_corequisites": [] + }, + "DSCI369": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 7.0, + "mean": 3.282600000000001, + "median": 3.282600000000001, + "min": 1.0, + "q1": 2.205781890939793, + "q3": 4.359418109060209, + "std_dev": 1.0768181090602076 + }, + "centrality": { + "max": 23.0, + "mean": 8.779899999999977, + "median": 8.779899999999977, + "min": 3.0, + "q1": 5.315097553972214, + "q3": 12.24470244602774, + "std_dev": 3.464802446027763 + }, + "complexity": { + "max": 11.0, + "mean": 7.080100000000011, + "median": 7.0, + "min": 4.0, + "q1": 6.0, + "q3": 8.0, + "std_dev": 1.32298298930863 + }, + "course_id": "DSCI369", + "delay": { + "max": 4.0, + "mean": 3.797500000000003, + "median": 3.797500000000003, + "min": 3.0, + "q1": 3.3956371502614386, + "q3": 4.0, + "std_dev": 0.40186284973856445 + }, + "plan_count": 10000 + }, + "name": "Linear Algebra for Data Science", + "number": "369", + "prefix": "DSCI", + "prerequisites": { + "or": [ + "MATH156", + "MATH160", + "MATH161" + ] + }, + "strict_corequisites": [] + }, + "ECE452": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 4.289743589743587, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 5.0, + "std_dev": 0.453643298138624 + }, + "course_id": "ECE452", + "delay": { + "max": 5.0, + "mean": 4.289743589743587, + "median": 4.289743589743587, + "min": 4.0, + "q1": 4.0, + "q3": 4.7433868878822105, + "std_dev": 0.453643298138624 + }, + "plan_count": 390 + }, + "name": "Computer Organization and Architecture", + "number": "452", + "prefix": "ECE", + "prerequisites": { + "or": [ + "CS250", + "CS270" + ] + }, + "strict_corequisites": [] + }, + "ENGR422": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "ENGR422", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 411 + }, + "name": "Technology Entrepreneurship", + "number": "422", + "prefix": "ENGR", + "strict_corequisites": [] + }, + "IDEA210": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "gen_ed_attributes": [ + "GT-AH1" + ], + "id": null, + "name": "Introduction to Design Thinking", + "number": "210", + "prefix": "IDEA", + "strict_corequisites": [] + }, + "IDEA310H": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "cross_listed_as": [ + "CS310H" + ], + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 5.272727272727277, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 6.0, + "std_dev": 0.44536177141512306 + }, + "course_id": "IDEA310H", + "delay": { + "max": 6.0, + "mean": 5.272727272727277, + "median": 5.272727272727277, + "min": 5.0, + "q1": 5.0, + "q3": 5.7180890441424, + "std_dev": 0.44536177141512306 + }, + "plan_count": 374 + }, + "name": "Design Thinking Toolbox: Mixed Reality Design", + "number": "310H", + "prefix": "IDEA", + "prerequisites": { + "or": [ + "CS214", + "CS253", + "IDEA210" + ] + }, + "strict_corequisites": [] + }, + "JTC372": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 1.0, + "mean": 0.4815256257449347, + "median": 0.4815256257449347, + "min": 0.0, + "q1": 0.0, + "q3": 0.9811842066738363, + "std_dev": 0.4996585809289017 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 3.0, + "mean": 1.9630512514898701, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 3.0, + "std_dev": 0.9993171618578035 + }, + "course_id": "JTC372", + "delay": { + "max": 2.0, + "mean": 1.4815256257449358, + "median": 1.4815256257449358, + "min": 1.0, + "q1": 1.0, + "q3": 1.9811842066738374, + "std_dev": 0.4996585809289017 + }, + "plan_count": 839 + }, + "name": "Web Design and Development", + "number": "372", + "prefix": "JTC", + "strict_corequisites": [] + }, + "JTC472": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 2.0, + "mean": 2.0, + "median": 2.0, + "min": 2.0, + "q1": 2.0, + "q3": 2.0, + "std_dev": 0.0 + }, + "course_id": "JTC472", + "delay": { + "max": 2.0, + "mean": 2.0, + "median": 2.0, + "min": 2.0, + "q1": 2.0, + "q3": 2.0, + "std_dev": 0.0 + }, + "plan_count": 404 + }, + "name": "Advanced Web Design and Development", + "number": "472", + "prefix": "JTC", + "prerequisites": "JTC372", + "strict_corequisites": [] + }, + "LIFE102": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "gen_ed_attributes": [ + "AUCC-3A" + ], + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "LIFE102", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 10000 + }, + "name": "Attributes of Living Systems", + "number": "102", + "prefix": "LIFE", + "strict_corequisites": [] + }, + "LIFE103": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "gen_ed_attributes": [ + "AUCC-3A" + ], + "id": null, + "name": "Biology of Organisms", + "number": "103", + "prefix": "LIFE", + "strict_corequisites": [] + }, + "MATH156": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "gen_ed_attributes": [ + "AUCC-1B", + "GT-MA1" + ], + "id": null, + "metrics": { + "blocking": { + "max": 23.0, + "mean": 8.926099999999975, + "median": 8.926099999999975, + "min": 0.0, + "q1": 2.582470219661273, + "q3": 15.269729780338677, + "std_dev": 6.343629780338702 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 30.0, + "mean": 13.41750000000003, + "median": 10.0, + "min": 1.0, + "q1": 8.0, + "q3": 12.0, + "std_dev": 7.821303839514235 + }, + "course_id": "MATH156", + "delay": { + "max": 7.0, + "mean": 4.491399999999983, + "median": 4.491399999999983, + "min": 1.0, + "q1": 2.9419723508340097, + "q3": 6.040827649165956, + "std_dev": 1.5494276491659729 + }, + "plan_count": 10000 + }, + "name": "Mathematics for Computational Science I (GT-MA1)", + "number": "156", + "prefix": "MATH", + "strict_corequisites": [] + }, + "MATH160": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "gen_ed_attributes": [ + "AUCC-1B", + "GT-MA1" + ], + "id": null, + "metrics": { + "blocking": { + "max": 22.0, + "mean": 10.02068965517242, + "median": 10.02068965517242, + "min": 3.0, + "q1": 3.9515830737397364, + "q3": 16.089796236605103, + "std_dev": 6.069106581432684 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 29.0, + "mean": 14.657471264367816, + "median": 11.0, + "min": 6.0, + "q1": 10.0, + "q3": 25.0, + "std_dev": 7.398347628253526 + }, + "course_id": "MATH160", + "delay": { + "max": 7.0, + "mean": 4.636781609195404, + "median": 4.636781609195404, + "min": 3.0, + "q1": 3.259952423174905, + "q3": 6.013610795215904, + "std_dev": 1.376829186020499 + }, + "plan_count": 435 + }, + "name": "Calculus for Physical Scientists I (GT-MA1)", + "number": "160", + "prefix": "MATH", + "strict_corequisites": [] + }, + "MATH161": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 2.0, + "mean": 2.0, + "median": 2.0, + "min": 2.0, + "q1": 2.0, + "q3": 2.0, + "std_dev": 0.0 + }, + "course_id": "MATH161", + "delay": { + "max": 2.0, + "mean": 2.0, + "median": 2.0, + "min": 2.0, + "q1": 2.0, + "q3": 2.0, + "std_dev": 0.0 + }, + "plan_count": 435 + }, + "name": "Calculus for Physical Scientists II (GT-MA1)", + "number": "161", + "prefix": "MATH", + "prerequisites": "MATH160", + "strict_corequisites": [] + }, + "MATH256": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 3.0, + "mean": 3.0, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 3.0, + "std_dev": 0.0 + }, + "course_id": "MATH256", + "delay": { + "max": 3.0, + "mean": 3.0, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 3.0, + "std_dev": 0.0 + }, + "plan_count": 431 + }, + "name": "Mathematics for Computational Science II", + "number": "256", + "prefix": "MATH", + "prerequisites": { + "and": [ + { + "or": [ + "MATH156", + "MATH161" + ] + }, + { + "or": [ + "DSCI369", + "MATH369" + ] + } + ] + }, + "strict_corequisites": [] + }, + "MATH369": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "name": "Linear Algebra I", + "number": "369", + "prefix": "MATH", + "prerequisites": { + "or": [ + "MATH156", + "MATH161" + ] + }, + "strict_corequisites": [] + }, + "MGT330": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "MGT330", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 440 + }, + "name": "Creativity, Innovation, and Value Creation", + "number": "330", + "prefix": "MGT", + "strict_corequisites": [] + }, + "MGT340": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "MGT340", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 473 + }, + "name": "Fundamentals of Entrepreneurship", + "number": "340", + "prefix": "MGT", + "strict_corequisites": [] + }, + "MGT420": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "MGT420", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 419 + }, + "name": "New Venture Creation", + "number": "420", + "prefix": "MGT", + "strict_corequisites": [] + }, + "PH141": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "gen_ed_attributes": [ + "AUCC-3A" + ], + "id": null, + "name": "Physics for Scientists & Engineers I", + "number": "141", + "prefix": "PH", + "strict_corequisites": [] + }, + "PH142": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "gen_ed_attributes": [ + "AUCC-3A" + ], + "id": null, + "name": "Physics for Scientists & Engineers II", + "number": "142", + "prefix": "PH", + "prerequisites": "PH141", + "strict_corequisites": [] + }, + "PHIL201": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "cross_listed_as": [ + "CS201" + ], + "csv_id": null, + "gen_ed_attributes": [ + "AUCC-3B", + "GT-AH3" + ], + "id": null, + "name": "Ethical Computing Systems (GT-AH3)", + "number": "201", + "prefix": "PHIL", + "strict_corequisites": [] + }, + "PHIL410": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 4.910284463894959, + "median": 5.0, + "min": 4.0, + "q1": 4.0, + "q3": 5.0, + "std_dev": 0.6537581359049474 + }, + "course_id": "PHIL410", + "delay": { + "max": 6.0, + "mean": 4.910284463894959, + "median": 4.910284463894959, + "min": 4.0, + "q1": 4.256526327990011, + "q3": 5.564042599799906, + "std_dev": 0.6537581359049474 + }, + "plan_count": 457 + }, + "name": "Godel's Incompleteness Theorems", + "number": "410", + "prefix": "PHIL", + "prerequisites": { + "or": [ + "CS220", + "CS253", + "CS270" + ] + }, + "strict_corequisites": [] + }, + "PHIL411": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "PHIL411", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 423 + }, + "name": "Logic in Philosophy and Beyond", + "number": "411", + "prefix": "PHIL", + "strict_corequisites": [] + }, + "PHIL415": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "PHIL415", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 446 + }, + "name": "Logic and Scientific Method", + "number": "415", + "prefix": "PHIL", + "strict_corequisites": [] + }, + "PSY252": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 2.0, + "mean": 0.8954859699064662, + "median": 0.8954859699064662, + "min": 0.0, + "q1": 0.48017318789808966, + "q3": 1.3107987519148427, + "std_dev": 0.41531278200837657 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 4.0, + "mean": 2.751525010166731, + "median": 3.0, + "min": 1.0, + "q1": 3.0, + "q3": 3.0, + "std_dev": 0.7440100456529356 + }, + "course_id": "PSY252", + "delay": { + "max": 2.0, + "mean": 1.8560390402602724, + "median": 1.8560390402602724, + "min": 1.0, + "q1": 1.5049888988124345, + "q3": 2.0, + "std_dev": 0.3510501414478379 + }, + "plan_count": 2459 + }, + "name": "Mind, Brain, and Behavior", + "number": "252", + "prefix": "PSY", + "strict_corequisites": [] + }, + "PSY352": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 2.0, + "mean": 2.0, + "median": 2.0, + "min": 2.0, + "q1": 2.0, + "q3": 2.0, + "std_dev": 0.0 + }, + "course_id": "PSY352", + "delay": { + "max": 2.0, + "mean": 2.0, + "median": 2.0, + "min": 2.0, + "q1": 2.0, + "q3": 2.0, + "std_dev": 0.0 + }, + "plan_count": 444 + }, + "name": "Learning and Memory", + "number": "352", + "prefix": "PSY", + "prerequisites": "PSY252", + "strict_corequisites": [] + }, + "PSY452": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 2.0, + "mean": 2.0, + "median": 2.0, + "min": 2.0, + "q1": 2.0, + "q3": 2.0, + "std_dev": 0.0 + }, + "course_id": "PSY452", + "delay": { + "max": 2.0, + "mean": 2.0, + "median": 2.0, + "min": 2.0, + "q1": 2.0, + "q3": 2.0, + "std_dev": 0.0 + }, + "plan_count": 425 + }, + "name": "Cognitive Psychology", + "number": "452", + "prefix": "PSY", + "prerequisites": "PSY252", + "strict_corequisites": [] + }, + "PSY454": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 2.0, + "mean": 2.0, + "median": 2.0, + "min": 2.0, + "q1": 2.0, + "q3": 2.0, + "std_dev": 0.0 + }, + "course_id": "PSY454", + "delay": { + "max": 2.0, + "mean": 2.0, + "median": 2.0, + "min": 2.0, + "q1": 2.0, + "q3": 2.0, + "std_dev": 0.0 + }, + "plan_count": 443 + }, + "name": "Biological Psychology", + "number": "454", + "prefix": "PSY", + "prerequisites": "PSY252", + "strict_corequisites": [] + }, + "PSY456": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 2.0, + "mean": 2.0, + "median": 2.0, + "min": 2.0, + "q1": 2.0, + "q3": 2.0, + "std_dev": 0.0 + }, + "course_id": "PSY456", + "delay": { + "max": 2.0, + "mean": 2.0, + "median": 2.0, + "min": 2.0, + "q1": 2.0, + "q3": 2.0, + "std_dev": 0.0 + }, + "plan_count": 444 + }, + "name": "Sensation and Perception", + "number": "456", + "prefix": "PSY", + "prerequisites": "PSY252", + "strict_corequisites": [] + }, + "PSY458": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 2.0, + "mean": 2.0, + "median": 2.0, + "min": 2.0, + "q1": 2.0, + "q3": 2.0, + "std_dev": 0.0 + }, + "course_id": "PSY458", + "delay": { + "max": 2.0, + "mean": 2.0, + "median": 2.0, + "min": 2.0, + "q1": 2.0, + "q3": 2.0, + "std_dev": 0.0 + }, + "plan_count": 446 + }, + "name": "Cognitive Neuroscience", + "number": "458", + "prefix": "PSY", + "prerequisites": "PSY252", + "strict_corequisites": [] + }, + "STAT301": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 4.0, + "mean": 2.2315789473684204, + "median": 2.2315789473684204, + "min": 1.0, + "q1": 1.2974017796912078, + "q3": 3.165756115045633, + "std_dev": 0.9341771676772125 + }, + "centrality": { + "max": 11.0, + "mean": 5.852631578947368, + "median": 5.852631578947368, + "min": 3.0, + "q1": 3.7302276734783506, + "q3": 7.975035484416385, + "std_dev": 2.1224039054690174 + }, + "complexity": { + "max": 8.0, + "mean": 5.68421052631579, + "median": 5.0, + "min": 4.0, + "q1": 4.0, + "q3": 7.0, + "std_dev": 1.3936937509611447 + }, + "course_id": "STAT301", + "delay": { + "max": 4.0, + "mean": 3.452631578947368, + "median": 3.452631578947368, + "min": 3.0, + "q1": 3.0, + "q3": 3.9503827544226056, + "std_dev": 0.4977511754752374 + }, + "plan_count": 95 + }, + "name": "Introduction to Applied Statistical Methods", + "number": "301", + "prefix": "STAT", + "prerequisites": "MATH160", + "strict_corequisites": [] + }, + "STAT302A": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 4.0, + "mean": 1.1583000000000065, + "median": 1.1583000000000065, + "min": 0.0, + "q1": 0.10909024499389153, + "q3": 2.2075097550061216, + "std_dev": 1.049209755006115 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 7.0, + "mean": 3.218900000000002, + "median": 3.0, + "min": 1.0, + "q1": 1.0, + "q3": 5.0, + "std_dev": 1.9342137394817591 + }, + "course_id": "STAT302A", + "delay": { + "max": 3.0, + "mean": 2.0605999999999978, + "median": 2.0605999999999978, + "min": 1.0, + "q1": 1.1556537916538865, + "q3": 2.965546208346109, + "std_dev": 0.9049462083461113 + }, + "plan_count": 10000 + }, + "name": "Statistics Supplement - General Applications", + "number": "302A", + "prefix": "STAT", + "strict_corequisites": [] + }, + "STAT307": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "name": "Introduction to Biostatistics", + "number": "307", + "prefix": "STAT", + "prerequisites": "MATH160", + "strict_corequisites": [] + }, + "STAT315": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 5.0, + "mean": 2.3018147086915, + "median": 2.3018147086915, + "min": 1.0, + "q1": 1.373317266813748, + "q3": 3.230312150569252, + "std_dev": 0.928497441877752 + }, + "centrality": { + "max": 15.0, + "mean": 6.04775549188157, + "median": 6.04775549188157, + "min": 3.0, + "q1": 3.8849430595637173, + "q3": 8.210567924199422, + "std_dev": 2.1628124323178524 + }, + "complexity": { + "max": 9.0, + "mean": 5.769340974212032, + "median": 5.0, + "min": 4.0, + "q1": 5.0, + "q3": 7.0, + "std_dev": 1.385142835094064 + }, + "course_id": "STAT315", + "delay": { + "max": 4.0, + "mean": 3.4675262655205366, + "median": 3.4675262655205366, + "min": 3.0, + "q1": 3.0, + "q3": 3.966470607676009, + "std_dev": 0.4989443421554724 + }, + "plan_count": 2094 + }, + "name": "Introduction to Theory and Practice of Statistics", + "number": "315", + "prefix": "STAT", + "prerequisites": { + "or": [ + "MATH156", + "MATH160" + ] + }, + "strict_corequisites": [] + } + } +} diff --git a/samples/degrees/neu-khoury-bscs-boston_report.json b/samples/degrees/neu-khoury-bscs-boston_report.json new file mode 100644 index 0000000..012e98d --- /dev/null +++ b/samples/degrees/neu-khoury-bscs-boston_report.json @@ -0,0 +1,6605 @@ +{ + "degree": { + "allow_double_counting": false, + "catalog_year": "2025-2026", + "cip_code": "11.0701", + "degree_type": "BS", + "gpa_major": 2.0, + "gpa_minimum": 2.0, + "grade_minimum": "D-", + "id": "northeastern-university-bachelor-of-science-in-computer-science-concentration", + "institution": "Northeastern University", + "major_subjects": [ + "CS", + "CY", + "DS", + "IS" + ], + "name": "Bachelor of Science in Computer Science (Boston)", + "source_url": "https://catalog.northeastern.edu/undergraduate/computer-information-science/computer-science/bscs/#programrequirementstext", + "system_type": "semester", + "tags": [ + "ai", + "ai-concentration" + ], + "total_credits": 134 + }, + "analysis": { + "metrics": { + "complexity": { + "max": 259.0, + "mean": 191.05789842732656, + "median": 191.0, + "min": 131.0, + "q1": 178.0, + "q3": 205.0, + "std_dev": 18.677582874782683 + }, + "credits": { + "max": 168.0, + "mean": 143.36862666533113, + "median": 143.36862666533113, + "min": 134.0, + "q1": 137.40596063882904, + "q3": 149.33129269183323, + "std_dev": 5.962666026502099 + }, + "delay": { + "max": 6.0, + "mean": 5.601422418110796, + "median": 6.0, + "min": 4.0, + "q1": 5.0, + "q3": 6.0, + "std_dev": 0.4950986656588116 + } + }, + "sample_type": "shuffled", + "variations_run": 9983 + }, + "requirements": { + "advanced_writing": { + "category": "gen_ed", + "courses": [ + "{ENGW3302, ENGW3315}" + ], + "name": "Advanced Writing in the Disciplines", + "tags": [ + "required" + ], + "type": "all" + }, + "computing_social_issues": { + "category": "supporting", + "count": 1, + "from": { + "courses": [ + "AFCS2600", + "CY4170", + "CY5240", + "{DS1300, PHIL1300}", + "HIST2220", + "INSH2102", + "JRNL3700", + "PHIL1145", + "SOCL1280", + "SOCL2485", + "SOCL4528" + ] + }, + "name": "Computing and Social Issues", + "tags": [ + "required" + ], + "type": "select" + }, + "concentration": { + "category": "major", + "name": "Computer Science Concentration", + "options": [ + { + "id": "artificial_intelligence", + "name": "Concentration in Artificial Intelligence", + "requirements": [ + { + "courses": [ + "CS4100", + "DS4400" + ], + "name": "AI Required Courses", + "type": "all" + }, + { + "count": 2, + "from": { + "courses": [ + "CS4120", + "CS4150", + "CS4180", + "CS4220", + "CS4610", + "CY4100", + "DS4420", + "DS4440" + ] + }, + "name": "AI Electives", + "type": "select" + } + ] + }, + { + "id": "foundations", + "name": "Concentration in Foundations", + "requirements": [ + { + "count": 2, + "from": { + "courses": [ + "{CS2800, CS4820}", + "{CS4805, CS4810}" + ] + }, + "name": "Foundations Required Courses", + "type": "select" + }, + { + "count": 2, + "from": { + "courses": [ + "CS3950", + "CS4950", + "CS4805", + "CS4810", + "CS4820", + "CS4830", + "CY4770" + ] + }, + "name": "Foundations Electives", + "type": "select" + } + ] + }, + { + "id": "human_centered_computing", + "name": "Concentration in Human-Centered Computing", + "requirements": [ + { + "courses": [ + "CS2484", + "CS3484" + ], + "name": "HCC Required Courses", + "type": "all" + }, + { + "count": 2, + "from": { + "courses": [ + "CS4097", + "CS4350", + "CS4520", + "CS4550", + "DS4200", + "HINF5300" + ] + }, + "name": "HCC Electives", + "type": "select" + } + ] + }, + { + "id": "software", + "name": "Concentration in Software", + "requirements": [ + { + "courses": [ + "CS2800", + "CS4400", + "{CS4700, CS4730}" + ], + "name": "Software Required Courses", + "type": "all" + }, + { + "count": 1, + "from": { + "courses": [ + "CS3520", + "CS4410", + "CS4520", + "CS4550", + "CS4700", + "CS4730", + "CS4820", + "CS4830" + ] + }, + "name": "Software Electives", + "type": "select" + } + ] + }, + { + "id": "systems", + "name": "Concentration in Systems", + "requirements": [ + { + "courses": [ + "{CS4700, CS4730}" + ], + "name": "Systems Required Networks", + "type": "all" + }, + { + "count": 1, + "from": { + "courses": [ + "CY3740", + "CY4740" + ] + }, + "name": "Systems Required Security", + "type": "select" + }, + { + "count": 2, + "from": { + "courses": [ + "CS3520", + "CS4300", + "CS4360", + "CS4610", + "CS4700", + "CS4730", + "CY3740", + "CY4740", + "CY4760" + ] + }, + "name": "Systems Electives", + "type": "select" + } + ] + } + ], + "tags": [ + "ai", + "required" + ], + "type": "one_of" + }, + "cs_fundamentals": { + "category": "major", + "courses": [ + "[CS1800, CS1802]", + "[CS2000, CS2001]", + "[CS2100, CS2101]" + ], + "name": "Computer Science Fundamental Courses", + "tags": [ + "core" + ], + "type": "all" + }, + "cs_overview": { + "category": "major", + "courses": [ + "CS1200", + "CS1210" + ], + "name": "Computer Science Overview", + "tags": [ + "core" + ], + "type": "all" + }, + "cs_required": { + "category": "major", + "courses": [ + "CS3000", + "[CS3100, CS3101]", + "CS3650", + "CS3800", + "{CS4530, CS4535}", + "DS3000" + ], + "name": "Computer Science Required Courses", + "tags": [ + "core" + ], + "type": "all" + }, + "electrical_engineering": { + "category": "supporting", + "courses": [ + "[EECE2310, EECE2311]" + ], + "name": "Digital Design and Computer Architecture", + "tags": [ + "required" + ], + "type": "all" + }, + "first_year_writing": { + "category": "gen_ed", + "courses": [ + "ENGW1111" + ], + "name": "College Writing", + "tags": [ + "required" + ], + "type": "all" + }, + "general_electives": { + "category": "elective", + "credits": 28, + "from": { + "include": [ + "*:*" + ] + }, + "name": "Required General Electives (NUpath gen-ed and free electives)", + "tags": [ + "elective", + "gen_ed" + ], + "type": "select" + }, + "khoury_electives": { + "category": "major", + "credits": 8, + "from": { + "courses": [ + "MKTG4606" + ], + "include": [ + "CS:2500+", + "CY:2000+", + "DS:2500+" + ] + }, + "name": "Khoury Approved Electives", + "tags": [ + "elective" + ], + "type": "select" + }, + "mathematics": { + "category": "supporting", + "courses": [ + "MATH1341", + "{MATH1365, MATH1465}" + ], + "name": "Mathematics Courses", + "tags": [ + "core" + ], + "type": "all" + }, + "presentation_requirement": { + "category": "major", + "count": 1, + "from": { + "courses": [ + "COMM1112", + "COMM1113", + "COMM1210", + "COMM1511", + "THTR1125", + "THTR1130", + "THTR1180", + "THTR2345" + ] + }, + "name": "Presentation Requirement", + "tags": [ + "required" + ], + "type": "select" + }, + "science_requirement": { + "category": "supporting", + "count": 2, + "from": { + "courses": [ + "[BIOL1111, BIOL1112]", + "[BIOL1113, BIOL1114]", + "[BIOL2301, BIOL2302]", + "[CHEM1161, CHEM1162]", + "[CHEM1211, CHEM1212]", + "[CHEM1214, CHEM1215]", + "[ENVR1200, ENVR1201]", + "[ENVR2310, ENVR2311]", + "[ENVR2340, ENVR2341]", + "[ENVR3300, ENVR3301]", + "[ENVR4500, ENVR4501]", + "MATH1342", + "MATH2280", + "MATH2331", + "MATH3081", + "[PHYS1145, PHYS1146]", + "[PHYS1147, PHYS1148]", + "[PHYS1151, PHYS1152]", + "[PHYS1155, PHYS1156]", + "[PHYS1161, PHYS1162]", + "[PHYS1165, PHYS1166]" + ] + }, + "name": "Science Requirement", + "tags": [ + "required" + ], + "type": "select" + }, + "security_requirement": { + "category": "major", + "count": 1, + "from": { + "courses": [ + "CY2550", + "CY3740", + "CY4740" + ] + }, + "name": "Security Required Course", + "tags": [ + "required" + ], + "type": "select" + } + }, + "selected_plans": [ + { + "category": "Shortest Path", + "course_count": 44, + "credits": 134.0, + "critical_path": [ + "CS2000", + "CS2100", + "DS3000" + ], + "is_calc_ready": false, + "longest_delay": 4, + "schedule": [ + { + "courses": [ + "CS2000", + "CS2001", + "MATH1341", + "CS1800", + "CS1802", + "GE09" + ], + "credits": 17.0, + "term": 1 + }, + { + "courses": [ + "CS2100", + "CS2101", + "MATH1342", + "GE01", + "GE05", + "THTR1125" + ], + "credits": 19.0, + "term": 2 + }, + { + "courses": [ + "CS2800", + "CS3000", + "CS3100", + "CS3101", + "CS1210", + "GE10S" + ], + "credits": 16.0, + "term": 3 + }, + { + "courses": [ + "CS3800", + "MATH2280", + "CS3650", + "GE06" + ], + "credits": 15.0, + "term": 4 + }, + { + "courses": [ + "CS4530", + "CS4805", + "CS4820", + "GE07" + ], + "credits": 15.0, + "term": 5 + }, + { + "courses": [ + "DS3000", + "CY2550", + "ELEC003", + "GE03", + "MATH1365" + ], + "credits": 18.0, + "term": 6 + }, + { + "courses": [ + "AFCS2600", + "ELEC001", + "ELEC002", + "GE02", + "GE08" + ], + "credits": 16.0, + "term": 7 + }, + { + "courses": [ + "CS1200", + "CS2501", + "DS2501", + "EECE2310", + "EECE2311", + "ELEC004", + "GE04", + "MKTG4606" + ], + "credits": 18.0, + "term": 8 + } + ], + "terms_required": 8, + "total_complexity": 131 + }, + { + "category": "Longest Path", + "course_count": 48, + "credits": 157.0, + "critical_path": [ + "CS2000", + "CS2100", + "DS3000", + "DS4400", + "DS4420" + ], + "is_calc_ready": false, + "longest_delay": 6, + "schedule": [ + { + "courses": [ + "CS2000", + "CS2001", + "MATH1341", + "CS1800", + "CS1802" + ], + "credits": 14.0, + "term": 1 + }, + { + "courses": [ + "CS2100", + "CS2101", + "MATH1342", + "EECE2310", + "EECE2311" + ], + "credits": 14.0, + "term": 2 + }, + { + "courses": [ + "CS3100", + "CS3101", + "MATH2280", + "CS3000", + "CS1210", + "CS3800" + ], + "credits": 18.0, + "term": 3 + }, + { + "courses": [ + "CS3650", + "DS3000", + "CS4400", + "CS4530", + "GE04" + ], + "credits": 19.0, + "term": 4 + }, + { + "courses": [ + "CY4740", + "DS4400", + "CS3540", + "COMM1112", + "GE05" + ], + "credits": 19.0, + "term": 5 + }, + { + "courses": [ + "CY4100", + "CY4760", + "DS4420", + "CS4097", + "GE06" + ], + "credits": 19.0, + "term": 6 + }, + { + "courses": [ + "CS4360", + "CS4410", + "CS4700", + "CS1200", + "GE01", + "GE07" + ], + "credits": 19.0, + "term": 7 + }, + { + "courses": [ + "CS4730", + "CY3740", + "DS4200", + "DS2501", + "GE02", + "GE08" + ], + "credits": 19.0, + "term": 8 + }, + { + "courses": [ + "MATH1365", + "GE03", + "GE09", + "AFCS2600", + "GE10S" + ], + "credits": 16.0, + "term": 9 + } + ], + "terms_required": 9, + "total_complexity": 259 + }, + { + "category": "Random Sample", + "course_count": 46, + "credits": 150.0, + "critical_path": [ + "CS2000", + "CS2100", + "CS3100", + "CS3650", + "CY3740", + "CY4770" + ], + "is_calc_ready": false, + "longest_delay": 6, + "schedule": [ + { + "courses": [ + "CS2000", + "CS2001", + "MATH1341", + "CS1800", + "CS1802", + "GE05", + "MATH1365" + ], + "credits": 21.0, + "term": 1 + }, + { + "courses": [ + "CS2100", + "CS2101", + "MATH1342", + "AFCS2600", + "GE03", + "GE09" + ], + "credits": 19.0, + "term": 2 + }, + { + "courses": [ + "CS3100", + "CS3101", + "MATH2280", + "CS2800", + "CS1210", + "DS2500" + ], + "credits": 18.0, + "term": 3 + }, + { + "courses": [ + "CS3650", + "DS3000", + "CS3000", + "CS1200", + "GE04", + "GE10S" + ], + "credits": 18.0, + "term": 4 + }, + { + "courses": [ + "CY3740", + "CS3800", + "CS4700", + "CS3950", + "GE06", + "THTR2345" + ], + "credits": 21.0, + "term": 5 + }, + { + "courses": [ + "CY4770", + "DS4400", + "CS4530", + "EECE2310", + "EECE2311" + ], + "credits": 17.0, + "term": 6 + }, + { + "courses": [ + "CS4535", + "CS4550", + "CS4805", + "GE01", + "GE07" + ], + "credits": 18.0, + "term": 7 + }, + { + "courses": [ + "CS4820", + "CS3500", + "CY2550", + "GE02", + "GE08" + ], + "credits": 18.0, + "term": 8 + } + ], + "terms_required": 8, + "total_complexity": 203 + }, + { + "category": "Random Sample", + "course_count": 44, + "credits": 141.0, + "critical_path": [ + "MATH1341", + "MATH1342", + "MATH2280", + "DS3000", + "DS4400", + "DS4420" + ], + "is_calc_ready": false, + "longest_delay": 6, + "schedule": [ + { + "courses": [ + "MATH1341", + "CS2000", + "CS2001", + "CS1800", + "CS1802", + "GE06" + ], + "credits": 17.0, + "term": 1 + }, + { + "courses": [ + "MATH1342", + "CS2100", + "CS2101", + "COMM1210", + "GE03", + "GE10S" + ], + "credits": 18.0, + "term": 2 + }, + { + "courses": [ + "MATH2280", + "CS3100", + "CS3101", + "CS2800", + "CS1210", + "GE07" + ], + "credits": 17.0, + "term": 3 + }, + { + "courses": [ + "DS3000", + "CS3650", + "CS3000", + "CS1200", + "GE04", + "MATH1365" + ], + "credits": 20.0, + "term": 4 + }, + { + "courses": [ + "DS4400", + "CS4350", + "CS4700", + "CS2511", + "GE05" + ], + "credits": 16.0, + "term": 5 + }, + { + "courses": [ + "DS4420", + "CY3740", + "DS4200", + "EECE2310", + "EECE2311" + ], + "credits": 17.0, + "term": 6 + }, + { + "courses": [ + "CS4400", + "CS4530", + "CS4820", + "GE01", + "GE08" + ], + "credits": 18.0, + "term": 7 + }, + { + "courses": [ + "CS3800", + "DS2500", + "AFCS2600", + "GE02", + "GE09" + ], + "credits": 18.0, + "term": 8 + } + ], + "terms_required": 8, + "total_complexity": 204 + }, + { + "category": "Random Sample", + "course_count": 45, + "credits": 148.0, + "critical_path": [ + "MATH1341", + "MATH1342", + "MATH2280", + "DS3000", + "DS4400", + "DS4440" + ], + "is_calc_ready": false, + "longest_delay": 6, + "schedule": [ + { + "courses": [ + "MATH1341", + "CS2000", + "CS2001", + "CS1800", + "CS1802", + "GE04", + "MATH1365" + ], + "credits": 21.0, + "term": 1 + }, + { + "courses": [ + "MATH1342", + "CS2100", + "CS2101", + "AFCS2600", + "GE02", + "GE07" + ], + "credits": 19.0, + "term": 2 + }, + { + "courses": [ + "MATH2280", + "CS3100", + "CS3101", + "CS3540", + "CS1210", + "GE05" + ], + "credits": 17.0, + "term": 3 + }, + { + "courses": [ + "DS3000", + "CS3650", + "CS4097", + "COMM1511", + "GE08" + ], + "credits": 19.0, + "term": 4 + }, + { + "courses": [ + "DS4400", + "CS4360", + "CY4740", + "CS1200", + "GE03", + "GE09" + ], + "credits": 19.0, + "term": 5 + }, + { + "courses": [ + "CY4100", + "DS4420", + "DS4440", + "CS2484", + "GE10S" + ], + "credits": 18.0, + "term": 6 + }, + { + "courses": [ + "CS4530", + "CS4550", + "CS4610", + "EECE2310", + "EECE2311" + ], + "credits": 17.0, + "term": 7 + }, + { + "courses": [ + "CS3000", + "CS3484", + "CS3800", + "GE01", + "GE06" + ], + "credits": 18.0, + "term": 8 + } + ], + "terms_required": 8, + "total_complexity": 213 + }, + { + "category": "Random Sample", + "course_count": 46, + "credits": 152.0, + "critical_path": [ + "CS2000", + "CS2100", + "DS3000", + "DS4400", + "DS4420" + ], + "is_calc_ready": false, + "longest_delay": 6, + "schedule": [ + { + "courses": [ + "CS2000", + "CS2001", + "CS1800", + "CS1802", + "MATH1341", + "GE04", + "GE10S" + ], + "credits": 19.0, + "term": 1 + }, + { + "courses": [ + "CS2100", + "CS2101", + "MATH1342", + "AFCS2600", + "GE02", + "GE06" + ], + "credits": 19.0, + "term": 2 + }, + { + "courses": [ + "CS2810", + "CS3100", + "CS3101", + "CS3000", + "CS1210", + "CS2800" + ], + "credits": 18.0, + "term": 3 + }, + { + "courses": [ + "DS3000", + "CS3650", + "CS3520", + "MATH2280", + "GE07" + ], + "credits": 19.0, + "term": 4 + }, + { + "courses": [ + "DS4400", + "CS3800", + "CS4350", + "COMM1112", + "GE08" + ], + "credits": 19.0, + "term": 5 + }, + { + "courses": [ + "DS4420", + "CY3740", + "CY4740", + "CS1200", + "GE03", + "GE09" + ], + "credits": 19.0, + "term": 6 + }, + { + "courses": [ + "CS4150", + "CS4400", + "CS4530", + "EECE2310", + "EECE2311", + "MATH1365" + ], + "credits": 21.0, + "term": 7 + }, + { + "courses": [ + "CS4805", + "CS4810", + "CS4830", + "GE01", + "GE05" + ], + "credits": 18.0, + "term": 8 + } + ], + "terms_required": 8, + "total_complexity": 216 + }, + { + "category": "Random Sample", + "course_count": 44, + "credits": 144.0, + "critical_path": [ + "CS2000", + "CS2100", + "CS3100", + "CS4400", + "CS4410" + ], + "is_calc_ready": false, + "longest_delay": 5, + "schedule": [ + { + "courses": [ + "CS2000", + "CS2001", + "CS1800", + "CS1802", + "MATH1341", + "GE05" + ], + "credits": 17.0, + "term": 1 + }, + { + "courses": [ + "CS2100", + "CS2101", + "MATH1342", + "COMM1112", + "GE03", + "GE09" + ], + "credits": 19.0, + "term": 2 + }, + { + "courses": [ + "CS3000", + "CS3100", + "CS3101", + "MATH2280", + "CS1210", + "GE06" + ], + "credits": 17.0, + "term": 3 + }, + { + "courses": [ + "CS3650", + "CS4400", + "CS4100", + "CS1200", + "GE04", + "GE10S" + ], + "credits": 18.0, + "term": 4 + }, + { + "courses": [ + "CS4410", + "CS4700", + "CS4730", + "CS2500", + "MATH1365" + ], + "credits": 20.0, + "term": 5 + }, + { + "courses": [ + "CY3740", + "CY4740", + "CS4530", + "EECE2310", + "EECE2311" + ], + "credits": 17.0, + "term": 6 + }, + { + "courses": [ + "CS4610", + "DS3000", + "CS2800", + "GE01", + "GE07" + ], + "credits": 18.0, + "term": 7 + }, + { + "courses": [ + "CS3800", + "CY2550", + "AFCS2600", + "GE02", + "GE08" + ], + "credits": 18.0, + "term": 8 + } + ], + "terms_required": 8, + "total_complexity": 183 + } + ], + "courses": { + "AFCS2600": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "AFCS2600", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 9983 + }, + "name": "Issues in Race, Science, and Technology", + "number": "2600", + "prefix": "AFCS", + "strict_corequisites": [] + }, + "BIOL1111": { + "canonical_name": null, + "corequisites": [ + "BIOL1112" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "General Biology 1", + "number": "1111", + "prefix": "BIOL", + "strict_corequisites": [ + "BIOL1112" + ] + }, + "BIOL1112": { + "canonical_name": null, + "corequisites": [ + "BIOL1111" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "name": "Lab for BIOL 1111", + "number": "1112", + "prefix": "BIOL", + "strict_corequisites": [ + "BIOL1111" + ] + }, + "BIOL1113": { + "canonical_name": null, + "corequisites": [ + "BIOL1114" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "General Biology 2", + "number": "1113", + "prefix": "BIOL", + "prerequisites": "BIOL1111", + "strict_corequisites": [ + "BIOL1114" + ] + }, + "BIOL1114": { + "canonical_name": null, + "corequisites": [ + "BIOL1113" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "name": "Lab for BIOL 1113", + "number": "1114", + "prefix": "BIOL", + "strict_corequisites": [ + "BIOL1113" + ] + }, + "BIOL2301": { + "canonical_name": null, + "corequisites": [ + "BIOL2302" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Genetics and Molecular Biology", + "number": "2301", + "prefix": "BIOL", + "prerequisites": "BIOL1111", + "strict_corequisites": [ + "BIOL2302" + ] + }, + "BIOL2302": { + "canonical_name": null, + "corequisites": [ + "BIOL2301" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "name": "Lab for BIOL 2301", + "number": "2302", + "prefix": "BIOL", + "strict_corequisites": [ + "BIOL2301" + ] + }, + "CHEM1161": { + "canonical_name": null, + "corequisites": [ + "CHEM1162" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "General Chemistry for Science Majors", + "number": "1161", + "prefix": "CHEM", + "strict_corequisites": [ + "CHEM1162" + ] + }, + "CHEM1162": { + "canonical_name": null, + "corequisites": [ + "CHEM1161" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "name": "Lab for CHEM 1161", + "number": "1162", + "prefix": "CHEM", + "strict_corequisites": [ + "CHEM1161" + ] + }, + "CHEM1211": { + "canonical_name": null, + "corequisites": [ + "CHEM1212" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "General Chemistry 1", + "number": "1211", + "prefix": "CHEM", + "strict_corequisites": [ + "CHEM1212" + ] + }, + "CHEM1212": { + "canonical_name": null, + "corequisites": [ + "CHEM1211" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "name": "Lab for CHEM 1211", + "number": "1212", + "prefix": "CHEM", + "strict_corequisites": [ + "CHEM1211" + ] + }, + "CHEM1214": { + "canonical_name": null, + "corequisites": [ + "CHEM1215" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "General Chemistry 2", + "number": "1214", + "prefix": "CHEM", + "prerequisites": "CHEM1211", + "strict_corequisites": [ + "CHEM1215" + ] + }, + "CHEM1215": { + "canonical_name": null, + "corequisites": [ + "CHEM1214" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "name": "Lab for CHEM 1214", + "number": "1215", + "prefix": "CHEM", + "strict_corequisites": [ + "CHEM1214" + ] + }, + "COMM1112": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "COMM1112", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 1230 + }, + "name": "Public Speaking", + "number": "1112", + "prefix": "COMM", + "strict_corequisites": [] + }, + "COMM1113": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "COMM1113", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 1257 + }, + "name": "Business and Professional Speaking", + "number": "1113", + "prefix": "COMM", + "strict_corequisites": [] + }, + "COMM1210": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "COMM1210", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 1267 + }, + "name": "Persuasion and Rhetoric", + "number": "1210", + "prefix": "COMM", + "strict_corequisites": [] + }, + "COMM1511": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "COMM1511", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 1233 + }, + "name": "Communication and Storytelling", + "number": "1511", + "prefix": "COMM", + "strict_corequisites": [] + }, + "CS1200": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "CS1200", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 9983 + }, + "name": "First Year Seminar", + "number": "1200", + "prefix": "CS", + "strict_corequisites": [] + }, + "CS1210": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 3.0, + "mean": 3.0, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 3.0, + "std_dev": 0.0 + }, + "course_id": "CS1210", + "delay": { + "max": 3.0, + "mean": 3.0, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 3.0, + "std_dev": 0.0 + }, + "plan_count": 9983 + }, + "name": "Professional Development for Khoury Co-op", + "number": "1210", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS2100", + "CS2510", + "DS2500" + ] + }, + "strict_corequisites": [] + }, + "CS1800": { + "canonical_name": null, + "corequisites": [ + "CS1802" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 12.0, + "mean": 4.158168887108041, + "median": 4.158168887108041, + "min": 2.0, + "q1": 2.3780812648184035, + "q3": 5.938256509397679, + "std_dev": 1.7800876222896376 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 17.0, + "mean": 7.087649003305608, + "median": 7.0, + "min": 4.0, + "q1": 5.0, + "q3": 8.0, + "std_dev": 2.3678179548658935 + }, + "course_id": "CS1800", + "delay": { + "max": 5.0, + "mean": 2.929480116197539, + "median": 2.929480116197539, + "min": 2.0, + "q1": 2.21610747088294, + "q3": 3.642852761512138, + "std_dev": 0.7133726453145992 + }, + "plan_count": 9983 + }, + "name": "Discrete Structures", + "number": "1800", + "prefix": "CS", + "strict_corequisites": [ + "CS1802" + ] + }, + "CS1802": { + "canonical_name": null, + "corequisites": [ + "CS1800" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "CS1802", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 9983 + }, + "name": "Seminar for CS 1800", + "number": "1802", + "prefix": "CS", + "strict_corequisites": [ + "CS1800" + ] + }, + "CS2000": { + "canonical_name": null, + "corequisites": [ + "CS2001" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 24.0, + "mean": 17.693278573575054, + "median": 17.693278573575054, + "min": 1.0, + "q1": 15.73245780410589, + "q3": 19.654099343044216, + "std_dev": 1.9608207694691637 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 29.0, + "mean": 23.045877992587346, + "median": 23.0, + "min": 3.0, + "q1": 22.0, + "q3": 24.0, + "std_dev": 2.1805921805275092 + }, + "course_id": "CS2000", + "delay": { + "max": 6.0, + "mean": 5.352599419012309, + "median": 5.352599419012309, + "min": 2.0, + "q1": 4.799784321341827, + "q3": 5.905414516682791, + "std_dev": 0.5528150976704818 + }, + "plan_count": 9983 + }, + "name": "Introduction to Program Design and Implementation", + "number": "2000", + "prefix": "CS", + "strict_corequisites": [ + "CS2001" + ] + }, + "CS2001": { + "canonical_name": null, + "corequisites": [ + "CS2000" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "CS2001", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 9983 + }, + "name": "Lab for CS 2000", + "number": "2001", + "prefix": "CS", + "strict_corequisites": [ + "CS2000" + ] + }, + "CS2100": { + "canonical_name": null, + "corequisites": [ + "CS2101" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 23.0, + "mean": 16.693278573575054, + "median": 16.693278573575054, + "min": 0.0, + "q1": 14.73245780410589, + "q3": 18.654099343044216, + "std_dev": 1.9608207694691637 + }, + "centrality": { + "max": 99.0, + "mean": 53.783131323249485, + "median": 53.783131323249485, + "min": 0.0, + "q1": 44.261089225190744, + "q3": 63.305173421308226, + "std_dev": 9.522042098058744 + }, + "complexity": { + "max": 28.0, + "mean": 22.045877992587346, + "median": 22.0, + "min": 2.0, + "q1": 21.0, + "q3": 23.0, + "std_dev": 2.1805921805275092 + }, + "course_id": "CS2100", + "delay": { + "max": 6.0, + "mean": 5.352599419012309, + "median": 5.352599419012309, + "min": 2.0, + "q1": 4.799784321341827, + "q3": 5.905414516682791, + "std_dev": 0.5528150976704818 + }, + "plan_count": 9983 + }, + "name": "Program Design and Implementation 1", + "number": "2100", + "prefix": "CS", + "prerequisites": "CS2000", + "strict_corequisites": [ + "CS2101" + ] + }, + "CS2101": { + "canonical_name": null, + "corequisites": [ + "CS2100" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "CS2101", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 9983 + }, + "name": "Lab for CS 2100", + "number": "2101", + "prefix": "CS", + "strict_corequisites": [ + "CS2100" + ] + }, + "CS2484": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "CS2484", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 973 + }, + "name": "Principles of Human-Computer Interaction", + "number": "2484", + "prefix": "CS", + "strict_corequisites": [] + }, + "CS2500": { + "canonical_name": null, + "corequisites": [ + "CS2501" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 19.0, + "mean": 0.7728651928912016, + "median": 0.7728651928912016, + "min": 0.0, + "q1": 0.0, + "q3": 2.6502257339823894, + "std_dev": 1.877360541091188 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 24.0, + "mean": 2.3849154746423924, + "median": 3.0, + "min": 1.0, + "q1": 1.0, + "q3": 3.0, + "std_dev": 2.482552472501751 + }, + "course_id": "CS2500", + "delay": { + "max": 6.0, + "mean": 1.6120502817511921, + "median": 1.6120502817511921, + "min": 1.0, + "q1": 1.0, + "q3": 2.2936510017246063, + "std_dev": 0.6816007199734142 + }, + "plan_count": 2307 + }, + "name": "Fundamentals of Computer Science 1", + "number": "2500", + "prefix": "CS", + "strict_corequisites": [] + }, + "CS2501": { + "canonical_name": null, + "corequisites": [ + "CS2500" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "CS2501", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 1233 + }, + "name": "Lab for CS 2500", + "number": "2501", + "prefix": "CS", + "strict_corequisites": [ + "CS2500" + ] + }, + "CS2510": { + "canonical_name": null, + "corequisites": [ + "CS2511" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 18.0, + "mean": 0.374710871241326, + "median": 0.374710871241326, + "min": 0.0, + "q1": 0.0, + "q3": 2.707456963390437, + "std_dev": 2.3327460921491108 + }, + "centrality": { + "max": 65.0, + "mean": 1.186584425597535, + "median": 1.186584425597535, + "min": 0.0, + "q1": 0.0, + "q3": 8.626337126707059, + "std_dev": 7.439752701109523 + }, + "complexity": { + "max": 23.0, + "mean": 2.4633770239013155, + "median": 2.0, + "min": 2.0, + "q1": 2.0, + "q3": 2.0, + "std_dev": 2.8819634798750173 + }, + "course_id": "CS2510", + "delay": { + "max": 6.0, + "mean": 2.0886661526599877, + "median": 2.0886661526599877, + "min": 2.0, + "q1": 2.0, + "q3": 2.6431762182429033, + "std_dev": 0.5545100655829158 + }, + "plan_count": 1297 + }, + "name": "Fundamentals of Computer Science 2", + "number": "2510", + "prefix": "CS", + "prerequisites": "CS2500", + "strict_corequisites": [] + }, + "CS2511": { + "canonical_name": null, + "corequisites": [ + "CS2510" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "CS2511", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 1203 + }, + "name": "Lab for CS 2510", + "number": "2511", + "prefix": "CS", + "strict_corequisites": [ + "CS2510" + ] + }, + "CS2800": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 1.0, + "mean": 0.4304149797570866, + "median": 0.4304149797570866, + "min": 0.0, + "q1": 0.0, + "q3": 0.9255492291862621, + "std_dev": 0.4951342494291756 + }, + "centrality": { + "max": 7.0, + "mean": 3.0129048582995983, + "median": 3.0129048582995983, + "min": 0.0, + "q1": 0.0, + "q3": 6.478844604303822, + "std_dev": 3.4659397460042234 + }, + "complexity": { + "max": 5.0, + "mean": 3.8608299595141577, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 5.0, + "std_dev": 0.9902684988583511 + }, + "course_id": "CS2800", + "delay": { + "max": 4.0, + "mean": 3.430414979757086, + "median": 3.430414979757086, + "min": 3.0, + "q1": 3.0, + "q3": 3.9255492291862617, + "std_dev": 0.4951342494291758 + }, + "plan_count": 3952 + }, + "name": "Logic and Computation", + "number": "2800", + "prefix": "CS", + "prerequisites": { + "and": [ + { + "or": [ + "CS1800", + "MATH1365", + "MATH2310" + ] + }, + { + "or": [ + "CS2100", + "CS2510" + ] + } + ] + }, + "strict_corequisites": [] + }, + "CS2810": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 7.0, + "mean": 1.8363072148952673, + "median": 1.8363072148952673, + "min": 0.0, + "q1": 0.0, + "q3": 3.851852083897861, + "std_dev": 2.0155448690025937 + }, + "centrality": { + "max": 51.0, + "mean": 10.925523661753317, + "median": 10.925523661753317, + "min": 0.0, + "q1": 0.0, + "q3": 23.110510108299096, + "std_dev": 12.184986446545777 + }, + "complexity": { + "max": 13.0, + "mean": 5.930178432893718, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 9.0, + "std_dev": 3.206340215684002 + }, + "course_id": "CS2810", + "delay": { + "max": 6.0, + "mean": 4.093871217998452, + "median": 4.093871217998452, + "min": 3.0, + "q1": 3.0, + "q3": 5.3059265102663, + "std_dev": 1.212055292267848 + }, + "plan_count": 1289 + }, + "name": "Mathematics of Data Models", + "number": "2810", + "prefix": "CS", + "prerequisites": { + "and": [ + "CS1800", + { + "or": [ + "CS2500", + "CS2100", + "DS2000" + ] + } + ] + }, + "strict_corequisites": [] + }, + "CS3000": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 5.0, + "mean": 0.9207653010117205, + "median": 0.9207653010117205, + "min": 0.0, + "q1": 0.0, + "q3": 1.9215325791821374, + "std_dev": 1.0007672781704169 + }, + "centrality": { + "max": 35.0, + "mean": 5.9094460582991095, + "median": 5.9094460582991095, + "min": 0.0, + "q1": 0.0, + "q3": 12.142234754947111, + "std_dev": 6.232788696648003 + }, + "complexity": { + "max": 10.0, + "mean": 4.596714414504673, + "median": 5.0, + "min": 3.0, + "q1": 3.0, + "q3": 6.0, + "std_dev": 1.6092357225147707 + }, + "course_id": "CS3000", + "delay": { + "max": 5.0, + "mean": 3.67594911349294, + "median": 3.67594911349294, + "min": 3.0, + "q1": 3.0176130830539076, + "q3": 4.3342851439319725, + "std_dev": 0.6583360304390323 + }, + "plan_count": 9983 + }, + "name": "Algorithms and Data", + "number": "3000", + "prefix": "CS", + "prerequisites": { + "and": [ + { + "or": [ + "CS2100", + "CS2510", + "DS2500" + ] + }, + "CS1800" + ] + }, + "strict_corequisites": [] + }, + "CS3100": { + "canonical_name": null, + "corequisites": [ + "CS3101" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 14.0, + "mean": 8.036061304217206, + "median": 8.036061304217206, + "min": 2.0, + "q1": 6.198170086526155, + "q3": 9.873952521908256, + "std_dev": 1.8378912176910507 + }, + "centrality": { + "max": 51.0, + "mean": 27.82119603325656, + "median": 27.82119603325656, + "min": 8.0, + "q1": 20.868703195557607, + "q3": 34.77368887095551, + "std_dev": 6.95249283769895 + }, + "complexity": { + "max": 20.0, + "mean": 13.383351697886395, + "median": 13.0, + "min": 6.0, + "q1": 12.0, + "q3": 15.0, + "std_dev": 2.0141220604186665 + }, + "course_id": "CS3100", + "delay": { + "max": 6.0, + "mean": 5.347290393669229, + "median": 5.347290393669229, + "min": 4.0, + "q1": 4.831007959964405, + "q3": 5.863572827374052, + "std_dev": 0.5162824337048237 + }, + "plan_count": 9983 + }, + "name": "Program Design and Implementation 2", + "number": "3100", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS2100", + "CS2510" + ] + }, + "strict_corequisites": [ + "CS3101" + ] + }, + "CS3101": { + "canonical_name": null, + "corequisites": [ + "CS3100" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "CS3101", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 9983 + }, + "name": "Lab for CS 3100", + "number": "3101", + "prefix": "CS", + "strict_corequisites": [ + "CS3100" + ] + }, + "CS3484": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 3.0, + "mean": 3.0, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 3.0, + "std_dev": 0.0 + }, + "course_id": "CS3484", + "delay": { + "max": 3.0, + "mean": 3.0, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 3.0, + "std_dev": 0.0 + }, + "plan_count": 2151 + }, + "name": "GUI Programming", + "number": "3484", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS2100", + "CS2510" + ] + }, + "strict_corequisites": [] + }, + "CS3500": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 3.0, + "mean": 3.0, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 3.0, + "std_dev": 0.0 + }, + "course_id": "CS3500", + "delay": { + "max": 3.0, + "mean": 3.0, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 3.0, + "std_dev": 0.0 + }, + "plan_count": 1044 + }, + "name": "Object-Oriented Design", + "number": "3500", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS2510", + "CS2100" + ] + }, + "strict_corequisites": [] + }, + "CS3520": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 1.0, + "mean": 0.48531011969532123, + "median": 0.48531011969532123, + "min": 0.0, + "q1": 0.0, + "q3": 0.9850942805254083, + "std_dev": 0.49978416083008703 + }, + "centrality": { + "max": 4.0, + "mean": 1.941240478781285, + "median": 1.941240478781285, + "min": 0.0, + "q1": 0.0, + "q3": 3.940377122101633, + "std_dev": 1.9991366433203481 + }, + "complexity": { + "max": 5.0, + "mean": 3.97062023939064, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 5.0, + "std_dev": 0.9995683216601742 + }, + "course_id": "CS3520", + "delay": { + "max": 4.0, + "mean": 3.4853101196953182, + "median": 3.4853101196953182, + "min": 3.0, + "q1": 3.0, + "q3": 3.9850942805254053, + "std_dev": 0.499784160830087 + }, + "plan_count": 3676 + }, + "name": "Programming in C++", + "number": "3520", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS2100", + "CS2510" + ] + }, + "strict_corequisites": [] + }, + "CS3540": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 1.0, + "mean": 0.5824701195219131, + "median": 0.5824701195219131, + "min": 0.0, + "q1": 0.08931833823542012, + "q3": 1.0, + "std_dev": 0.49315178128649295 + }, + "centrality": { + "max": 4.0, + "mean": 2.3298804780876523, + "median": 2.3298804780876523, + "min": 0.0, + "q1": 0.3572733529416805, + "q3": 4.0, + "std_dev": 1.9726071251459718 + }, + "complexity": { + "max": 5.0, + "mean": 4.164940239043831, + "median": 5.0, + "min": 3.0, + "q1": 3.0, + "q3": 5.0, + "std_dev": 0.986303562572986 + }, + "course_id": "CS3540", + "delay": { + "max": 4.0, + "mean": 3.5824701195219153, + "median": 3.5824701195219153, + "min": 3.0, + "q1": 3.0893183382354223, + "q3": 4.0, + "std_dev": 0.493151781286493 + }, + "plan_count": 2510 + }, + "name": "Game Programming", + "number": "3540", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS2100", + "CS2510" + ] + }, + "strict_corequisites": [] + }, + "CS3650": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 6.0, + "mean": 2.4357407592907947, + "median": 2.4357407592907947, + "min": 0.0, + "q1": 1.1903132736193023, + "q3": 3.6811682449622873, + "std_dev": 1.2454274856714924 + }, + "centrality": { + "max": 27.0, + "mean": 10.787338475408253, + "median": 10.787338475408253, + "min": 0.0, + "q1": 5.224087037491433, + "q3": 16.350589913325074, + "std_dev": 5.56325143791682 + }, + "complexity": { + "max": 12.0, + "mean": 7.729139537213263, + "median": 8.0, + "min": 4.0, + "q1": 7.0, + "q3": 9.0, + "std_dev": 1.6675991240366803 + }, + "course_id": "CS3650", + "delay": { + "max": 6.0, + "mean": 5.293398777922485, + "median": 5.293398777922485, + "min": 4.0, + "q1": 4.717440936943386, + "q3": 5.869356618901585, + "std_dev": 0.5759578409790997 + }, + "plan_count": 9983 + }, + "name": "Computer Systems", + "number": "3650", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS3100", + "CS3500" + ] + }, + "strict_corequisites": [] + }, + "CS3800": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 1.0, + "mean": 0.24832214765100696, + "median": 0.24832214765100696, + "min": 0.0, + "q1": 0.0, + "q3": 0.6803617970317059, + "std_dev": 0.4320396493806989 + }, + "centrality": { + "max": 7.0, + "mean": 1.738255033557052, + "median": 1.738255033557052, + "min": 0.0, + "q1": 0.0, + "q3": 4.762532579221951, + "std_dev": 3.0242775456648983 + }, + "complexity": { + "max": 5.0, + "mean": 3.4966442953020067, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 3.0, + "std_dev": 0.8640792987613973 + }, + "course_id": "CS3800", + "delay": { + "max": 4.0, + "mean": 3.248322147651014, + "median": 3.248322147651014, + "min": 3.0, + "q1": 3.0, + "q3": 3.6803617970317126, + "std_dev": 0.4320396493806988 + }, + "plan_count": 9983 + }, + "name": "Theory of Computation", + "number": "3800", + "prefix": "CS", + "prerequisites": { + "and": [ + { + "or": [ + "CS1800", + "MATH1365" + ] + }, + { + "or": [ + "CS2100", + "CS2510" + ] + } + ] + }, + "strict_corequisites": [] + }, + "CS3950": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 2.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 1.0, + "mean": 0.5614661044437232, + "median": 0.5614661044437232, + "min": 0.0, + "q1": 0.06525856922835244, + "q3": 1.0, + "std_dev": 0.4962075352153708 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 3.0, + "mean": 2.1229322088874496, + "median": 3.0, + "min": 1.0, + "q1": 1.0, + "q3": 3.0, + "std_dev": 0.9924150704307414 + }, + "course_id": "CS3950", + "delay": { + "max": 2.0, + "mean": 1.5614661044437248, + "median": 1.5614661044437248, + "min": 1.0, + "q1": 1.065258569228354, + "q3": 2.0, + "std_dev": 0.4962075352153707 + }, + "plan_count": 3083 + }, + "name": "Introduction to Computer Science Research", + "number": "3950", + "prefix": "CS", + "strict_corequisites": [] + }, + "CS4097": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "course_id": "CS4097", + "delay": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "plan_count": 1462 + }, + "name": "Special Topics in Computer Science", + "number": "4097", + "prefix": "CS", + "prerequisites": "CS3540", + "strict_corequisites": [] + }, + "CS4100": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 1.0, + "mean": 0.17268351383874858, + "median": 0.17268351383874858, + "min": 0.0, + "q1": 0.0, + "q3": 0.5506569492638476, + "std_dev": 0.37797343542509904 + }, + "centrality": { + "max": 5.0, + "mean": 0.8634175691937414, + "median": 0.8634175691937414, + "min": 0.0, + "q1": 0.0, + "q3": 2.7532847463192356, + "std_dev": 1.889867177125494 + }, + "complexity": { + "max": 6.0, + "mean": 4.345367027677499, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.7559468708501982 + }, + "course_id": "CS4100", + "delay": { + "max": 5.0, + "mean": 4.172683513838747, + "median": 4.172683513838747, + "min": 4.0, + "q1": 4.0, + "q3": 4.550656949263846, + "std_dev": 0.37797343542509904 + }, + "plan_count": 3324 + }, + "name": "Artificial Intelligence", + "number": "4100", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS3100", + "CS3500", + "DS3500" + ] + }, + "strict_corequisites": [], + "tags": [ + "ai" + ] + }, + "CS4120": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "course_id": "CS4120", + "delay": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "plan_count": 1705 + }, + "name": "Natural Language Processing", + "number": "4120", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS3100", + "CS3500" + ] + }, + "strict_corequisites": [], + "tags": [ + "ai" + ] + }, + "CS4150": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "course_id": "CS4150", + "delay": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "plan_count": 1784 + }, + "name": "Game Artificial Intelligence", + "number": "4150", + "prefix": "CS", + "prerequisites": { + "and": [ + { + "or": [ + "CS3100", + "CS3500" + ] + }, + "CS3520" + ] + }, + "strict_corequisites": [], + "tags": [ + "ai" + ] + }, + "CS4180": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 4.94819962097283, + "median": 5.0, + "min": 4.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.22162377977059636 + }, + "course_id": "CS4180", + "delay": { + "max": 5.0, + "mean": 4.94819962097283, + "median": 4.94819962097283, + "min": 4.0, + "q1": 4.726575841202234, + "q3": 5.0, + "std_dev": 0.22162377977059636 + }, + "plan_count": 1583 + }, + "name": "Reinforcement Learning", + "number": "4180", + "prefix": "CS", + "prerequisites": { + "and": [ + { + "or": [ + "CS3000", + "CS4800" + ] + }, + { + "or": [ + "CS2810", + "DS3000", + "MATH3081" + ] + } + ] + }, + "strict_corequisites": [], + "tags": [ + "ai" + ] + }, + "CS4220": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "CS4220", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 1552 + }, + "name": "Information Retrieval", + "number": "4220", + "prefix": "CS", + "prerequisites": { + "and": [ + { + "or": [ + "CS3100", + "CS3500" + ] + }, + "DS3000" + ] + }, + "strict_corequisites": [], + "tags": [ + "ai" + ] + }, + "CS4300": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "CS4300", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 2030 + }, + "name": "Computer Graphics", + "number": "4300", + "prefix": "CS", + "prerequisites": { + "and": [ + { + "or": [ + "CS3100", + "CS3500" + ] + }, + { + "or": [ + "DS3000", + "MATH2331" + ] + } + ] + }, + "strict_corequisites": [] + }, + "CS4350": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "CS4350", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 1622 + }, + "name": "Empirical Research Methods", + "number": "4350", + "prefix": "CS", + "prerequisites": "DS3000", + "strict_corequisites": [] + }, + "CS4360": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "CS4360", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 2265 + }, + "name": "Non-Interactive Computer Graphics", + "number": "4360", + "prefix": "CS", + "prerequisites": { + "and": [ + { + "or": [ + "DS3000", + "MATH2331" + ] + }, + { + "or": [ + "CS3100", + "CS3500" + ] + } + ] + }, + "strict_corequisites": [] + }, + "CS4400": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 1.0, + "mean": 0.44751180530330636, + "median": 0.44751180530330636, + "min": 0.0, + "q1": 0.0, + "q3": 0.9447491625256719, + "std_dev": 0.49723735722236556 + }, + "centrality": { + "max": 15.0, + "mean": 6.324010170722843, + "median": 6.324010170722843, + "min": 0.0, + "q1": 0.0, + "q3": 13.354352352854686, + "std_dev": 7.030342182131842 + }, + "complexity": { + "max": 7.0, + "mean": 5.006538321830731, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 6.0, + "std_dev": 1.0601047707796625 + }, + "course_id": "CS4400", + "delay": { + "max": 6.0, + "mean": 4.559026516527436, + "median": 4.559026516527436, + "min": 4.0, + "q1": 4.0, + "q3": 5.162521059418903, + "std_dev": 0.6034945428914665 + }, + "plan_count": 2753 + }, + "name": "Programming Languages", + "number": "4400", + "prefix": "CS", + "prerequisites": { + "and": [ + { + "or": [ + "CS3100", + "CS3500" + ] + }, + { + "or": [ + "CS3000", + "CS4800" + ] + } + ] + }, + "strict_corequisites": [] + }, + "CS4410": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 5.1314935064935, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.33793929076617996 + }, + "course_id": "CS4410", + "delay": { + "max": 6.0, + "mean": 5.1314935064935, + "median": 5.1314935064935, + "min": 5.0, + "q1": 5.0, + "q3": 5.46943279725968, + "std_dev": 0.33793929076617996 + }, + "plan_count": 1232 + }, + "name": "Compilers", + "number": "4410", + "prefix": "CS", + "prerequisites": "CS4400", + "strict_corequisites": [] + }, + "CS4520": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "course_id": "CS4520", + "delay": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "plan_count": 1727 + }, + "name": "Mobile Application Development", + "number": "4520", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS3100", + "CS3500" + ] + }, + "strict_corequisites": [] + }, + "CS4530": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "course_id": "CS4530", + "delay": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "plan_count": 9983 + }, + "name": "Fundamentals of Software Engineering", + "number": "4530", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS3100", + "CS3500" + ] + }, + "strict_corequisites": [] + }, + "CS4535": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "course_id": "CS4535", + "delay": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "plan_count": 1372 + }, + "name": "Professional Practicum Capstone", + "number": "4535", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS3100", + "CS3500" + ] + }, + "strict_corequisites": [] + }, + "CS4550": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "course_id": "CS4550", + "delay": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "plan_count": 1658 + }, + "name": "Web Development", + "number": "4550", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS3100", + "CS3500" + ] + }, + "strict_corequisites": [] + }, + "CS4610": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "course_id": "CS4610", + "delay": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "plan_count": 2615 + }, + "name": "Robotic Science and Systems", + "number": "4610", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS3100", + "CS3500" + ] + }, + "strict_corequisites": [], + "tags": [ + "ai" + ] + }, + "CS4700": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "CS4700", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 6028 + }, + "name": "Network Fundamentals", + "number": "4700", + "prefix": "CS", + "prerequisites": "CS3650", + "strict_corequisites": [] + }, + "CS4730": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "CS4730", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 2258 + }, + "name": "Distributed Systems", + "number": "4730", + "prefix": "CS", + "prerequisites": "CS3650", + "strict_corequisites": [] + }, + "CS4800": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 5.0, + "mean": 0.9166023166023177, + "median": 0.9166023166023177, + "min": 0.0, + "q1": 0.0, + "q3": 1.8828245306630582, + "std_dev": 0.9662222140607406 + }, + "centrality": { + "max": 34.0, + "mean": 6.582239382239395, + "median": 6.582239382239395, + "min": 0.0, + "q1": 0.0, + "q3": 13.321337055979523, + "std_dev": 6.739097673740128 + }, + "complexity": { + "max": 11.0, + "mean": 5.6239382239382305, + "median": 6.0, + "min": 4.0, + "q1": 4.0, + "q3": 7.0, + "std_dev": 1.5929036440882867 + }, + "course_id": "CS4800", + "delay": { + "max": 6.0, + "mean": 4.707335907335908, + "median": 4.707335907335908, + "min": 4.0, + "q1": 4.031166644708387, + "q3": 5.3835051699634295, + "std_dev": 0.6761692626275211 + }, + "plan_count": 1295 + }, + "name": "Algorithms and Data", + "number": "4800", + "prefix": "CS", + "prerequisites": { + "and": [ + { + "or": [ + "CS1800", + "MATH1365" + ] + }, + { + "or": [ + "CS3100", + "CS3500" + ] + } + ] + }, + "strict_corequisites": [] + }, + "CS4805": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "course_id": "CS4805", + "delay": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "plan_count": 2479 + }, + "name": "Fundamentals of Complexity Theory", + "number": "4805", + "prefix": "CS", + "prerequisites": "CS3800", + "strict_corequisites": [] + }, + "CS4810": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 4.0829199149539335, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.27576113333456864 + }, + "course_id": "CS4810", + "delay": { + "max": 5.0, + "mean": 4.0829199149539335, + "median": 4.0829199149539335, + "min": 4.0, + "q1": 4.0, + "q3": 4.358681048288502, + "std_dev": 0.27576113333456864 + }, + "plan_count": 1411 + }, + "name": "Advanced Algorithms", + "number": "4810", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS3000", + "CS4800" + ] + }, + "strict_corequisites": [] + }, + "CS4820": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 4.111111111111117, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.3142696805273543 + }, + "course_id": "CS4820", + "delay": { + "max": 5.0, + "mean": 4.111111111111117, + "median": 4.111111111111117, + "min": 4.0, + "q1": 4.0, + "q3": 4.425380791638471, + "std_dev": 0.3142696805273543 + }, + "plan_count": 1701 + }, + "name": "Computer-Aided Reasoning", + "number": "4820", + "prefix": "CS", + "prerequisites": { + "and": [ + "CS2800", + { + "or": [ + "CS3000", + "CS4800" + ] + } + ] + }, + "strict_corequisites": [] + }, + "CS4830": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 4.128310771041796, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.33443552005797866 + }, + "course_id": "CS4830", + "delay": { + "max": 5.0, + "mean": 4.128310771041796, + "median": 4.128310771041796, + "min": 4.0, + "q1": 4.0, + "q3": 4.462746291099775, + "std_dev": 0.33443552005797866 + }, + "plan_count": 1699 + }, + "name": "System Specification, Verification, and Synthesis", + "number": "4830", + "prefix": "CS", + "prerequisites": { + "or": [ + "CS3000", + "CS4800" + ] + }, + "strict_corequisites": [] + }, + "CS4950": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "max_repeat_credits": 2.0, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 2.0, + "mean": 2.0, + "median": 2.0, + "min": 2.0, + "q1": 2.0, + "q3": 2.0, + "std_dev": 0.0 + }, + "course_id": "CS4950", + "delay": { + "max": 2.0, + "mean": 2.0, + "median": 2.0, + "min": 2.0, + "q1": 2.0, + "q3": 2.0, + "std_dev": 0.0 + }, + "plan_count": 1731 + }, + "name": "Computer Science Research Seminar", + "number": "4950", + "prefix": "CS", + "prerequisites": "CS3950", + "repeatable": true, + "strict_corequisites": [] + }, + "CY2550": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 1.0, + "mean": 0.25494233937397065, + "median": 0.25494233937397065, + "min": 0.0, + "q1": 0.0, + "q3": 0.690771139441724, + "std_dev": 0.43582880006775326 + }, + "centrality": { + "max": 4.0, + "mean": 1.0197693574958826, + "median": 1.0197693574958826, + "min": 0.0, + "q1": 0.0, + "q3": 2.763084557766896, + "std_dev": 1.743315200271013 + }, + "complexity": { + "max": 5.0, + "mean": 3.5098846787479467, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 5.0, + "std_dev": 0.8716576001355067 + }, + "course_id": "CY2550", + "delay": { + "max": 4.0, + "mean": 3.254942339373963, + "median": 3.254942339373963, + "min": 3.0, + "q1": 3.0, + "q3": 3.6907711394417166, + "std_dev": 0.4358288000677535 + }, + "plan_count": 4856 + }, + "name": "Foundations of Cybersecurity", + "number": "2550", + "prefix": "CY", + "prerequisites": { + "or": [ + "CS2100", + "CS2500", + "DS2000", + "DS2500" + ] + }, + "strict_corequisites": [] + }, + "CY3740": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 2.0, + "mean": 0.2472139381572751, + "median": 0.2472139381572751, + "min": 0.0, + "q1": 0.0, + "q3": 0.7205888685638702, + "std_dev": 0.47337493040659506 + }, + "centrality": { + "max": 12.0, + "mean": 1.4832836289436528, + "median": 1.4832836289436528, + "min": 0.0, + "q1": 0.0, + "q3": 4.323533211383225, + "std_dev": 2.8402495824395717 + }, + "complexity": { + "max": 8.0, + "mean": 5.475435567414865, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.8841996848934773 + }, + "course_id": "CY3740", + "delay": { + "max": 6.0, + "mean": 5.228221629257581, + "median": 5.228221629257581, + "min": 5.0, + "q1": 5.0, + "q3": 5.647907842036357, + "std_dev": 0.4196862127787768 + }, + "plan_count": 6371 + }, + "name": "Systems Security", + "number": "3740", + "prefix": "CY", + "prerequisites": "CS3650", + "strict_corequisites": [] + }, + "CY4100": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 5.611967921036386, + "median": 6.0, + "min": 5.0, + "q1": 5.0, + "q3": 6.0, + "std_dev": 0.48730194403345767 + }, + "course_id": "CY4100", + "delay": { + "max": 6.0, + "mean": 5.611967921036386, + "median": 5.611967921036386, + "min": 5.0, + "q1": 5.124665977002928, + "q3": 6.0, + "std_dev": 0.48730194403345767 + }, + "plan_count": 1621 + }, + "name": "AI Security and Privacy", + "number": "4100", + "prefix": "CY", + "prerequisites": { + "or": [ + "CS4100", + "DS4400" + ] + }, + "strict_corequisites": [], + "tags": [ + "ai" + ] + }, + "CY4170": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "CY4170", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 1248 + }, + "name": "The Law, Ethics, and Policy of Data and Digital Technologies", + "number": "4170", + "prefix": "CY", + "strict_corequisites": [] + }, + "CY4740": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 2.0, + "mean": 0.39403345404380063, + "median": 0.39403345404380063, + "min": 0.0, + "q1": 0.0, + "q3": 0.9453543619476875, + "std_dev": 0.5513209079038869 + }, + "centrality": { + "max": 12.0, + "mean": 2.3642007242628016, + "median": 2.3642007242628016, + "min": 0.0, + "q1": 0.0, + "q3": 5.672126171686125, + "std_dev": 3.307925447423324 + }, + "complexity": { + "max": 8.0, + "mean": 5.755475081910691, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 7.0, + "std_dev": 1.0188145282455412 + }, + "course_id": "CY4740", + "delay": { + "max": 6.0, + "mean": 5.361441627866888, + "median": 5.361441627866888, + "min": 5.0, + "q1": 5.0, + "q3": 5.841859755659563, + "std_dev": 0.4804181277926743 + }, + "plan_count": 5799 + }, + "name": "Network Security", + "number": "4740", + "prefix": "CY", + "prerequisites": "CS3650", + "strict_corequisites": [] + }, + "CY4760": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 6.0, + "median": 6.0, + "min": 6.0, + "q1": 6.0, + "q3": 6.0, + "std_dev": 0.0 + }, + "course_id": "CY4760", + "delay": { + "max": 6.0, + "mean": 6.0, + "median": 6.0, + "min": 6.0, + "q1": 6.0, + "q3": 6.0, + "std_dev": 0.0 + }, + "plan_count": 2285 + }, + "name": "Security of Wireless and Mobile Systems", + "number": "4760", + "prefix": "CY", + "prerequisites": { + "or": [ + "CY3740", + "CY4740" + ] + }, + "strict_corequisites": [] + }, + "CY4770": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 6.0, + "median": 6.0, + "min": 6.0, + "q1": 6.0, + "q3": 6.0, + "std_dev": 0.0 + }, + "course_id": "CY4770", + "delay": { + "max": 6.0, + "mean": 6.0, + "median": 6.0, + "min": 6.0, + "q1": 6.0, + "q3": 6.0, + "std_dev": 0.0 + }, + "plan_count": 1575 + }, + "name": "Foundations of Cryptography", + "number": "4770", + "prefix": "CY", + "prerequisites": { + "or": [ + "CY3740", + "CY4740" + ] + }, + "strict_corequisites": [] + }, + "CY5240": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "course_id": "CY5240", + "delay": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "plan_count": 1238 + }, + "name": "Cyberlaw: Privacy, Ethics, and Digital Rights", + "number": "5240", + "prefix": "CY", + "prerequisites": "CY2550", + "strict_corequisites": [] + }, + "DS1300": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "cross_listed_as": [ + "PHIL1300" + ], + "csv_id": null, + "id": null, + "name": "Knowledge in a Digital World", + "number": "1300", + "prefix": "DS", + "strict_corequisites": [] + }, + "DS2000": { + "canonical_name": null, + "corequisites": [ + "DS2001" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Programming with Data", + "number": "2000", + "prefix": "DS", + "strict_corequisites": [ + "DS2001" + ] + }, + "DS2001": { + "canonical_name": null, + "corequisites": [ + "DS2000" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "name": "Data Science Programming Practicum", + "number": "2001", + "prefix": "DS", + "strict_corequisites": [ + "DS2000" + ] + }, + "DS2500": { + "canonical_name": null, + "corequisites": [ + "DS2501" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 1.0, + "mean": 0.49368686868686856, + "median": 0.49368686868686856, + "min": 0.0, + "q1": 0.0, + "q3": 0.9936470114712936, + "std_dev": 0.49996014278442513 + }, + "centrality": { + "max": 4.0, + "mean": 1.9747474747474743, + "median": 1.9747474747474743, + "min": 0.0, + "q1": 0.0, + "q3": 3.9745880458851746, + "std_dev": 1.9998405711377005 + }, + "complexity": { + "max": 5.0, + "mean": 3.9873737373737357, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 5.0, + "std_dev": 0.9999202855688503 + }, + "course_id": "DS2500", + "delay": { + "max": 4.0, + "mean": 3.4936868686868636, + "median": 3.4936868686868636, + "min": 3.0, + "q1": 3.0, + "q3": 3.993647011471289, + "std_dev": 0.4999601427844254 + }, + "plan_count": 2376 + }, + "name": "Intermediate Programming with Data", + "number": "2500", + "prefix": "DS", + "prerequisites": { + "or": [ + "DS2000", + "CS2100" + ] + }, + "strict_corequisites": [ + "DS2501" + ] + }, + "DS2501": { + "canonical_name": null, + "corequisites": [ + "DS2500" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "DS2501", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 1270 + }, + "name": "Lab for DS 2500", + "number": "2501", + "prefix": "DS", + "strict_corequisites": [ + "DS2500" + ] + }, + "DS3000": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 7.0, + "mean": 1.94170089151557, + "median": 1.94170089151557, + "min": 0.0, + "q1": 0.5478897141306454, + "q3": 3.3355120689004947, + "std_dev": 1.3938111773849247 + }, + "centrality": { + "max": 74.0, + "mean": 15.623459881799075, + "median": 15.623459881799075, + "min": 0.0, + "q1": 4.2689131134554135, + "q3": 26.97800665014274, + "std_dev": 11.354546768343662 + }, + "complexity": { + "max": 13.0, + "mean": 7.12160673144345, + "median": 7.0, + "min": 4.0, + "q1": 6.0, + "q3": 9.0, + "std_dev": 2.0114459968639187 + }, + "course_id": "DS3000", + "delay": { + "max": 6.0, + "mean": 5.179905839927867, + "median": 5.179905839927867, + "min": 4.0, + "q1": 4.480534396518924, + "q3": 5.879277283336809, + "std_dev": 0.6993714434089429 + }, + "plan_count": 9983 + }, + "name": "Foundations of Data Science", + "number": "3000", + "prefix": "DS", + "prerequisites": { + "and": [ + { + "or": [ + "CS2100", + "CS2510", + "DS2500" + ] + }, + { + "or": [ + "CS2810", + "DS2000", + "MATH2280" + ] + } + ] + }, + "strict_corequisites": [] + }, + "DS3500": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "course_id": "DS3500", + "delay": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "plan_count": 1173 + }, + "name": "Advanced Programming with Data", + "number": "3500", + "prefix": "DS", + "prerequisites": "DS2500", + "strict_corequisites": [] + }, + "DS4200": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "DS4200", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 1492 + }, + "name": "Information Presentation and Visualization", + "number": "4200", + "prefix": "DS", + "prerequisites": "DS3000", + "strict_corequisites": [] + }, + "DS4400": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 3.0, + "mean": 0.9527394395650374, + "median": 0.9527394395650374, + "min": 0.0, + "q1": 0.28795973131092767, + "q3": 1.617519147819147, + "std_dev": 0.6647797082541097 + }, + "centrality": { + "max": 63.0, + "mean": 15.444165621079055, + "median": 15.444165621079055, + "min": 0.0, + "q1": 4.597330818984547, + "q3": 26.291000423173564, + "std_dev": 10.846834802094508 + }, + "complexity": { + "max": 9.0, + "mean": 6.678377248013384, + "median": 7.0, + "min": 4.0, + "q1": 6.0, + "q3": 7.0, + "std_dev": 1.0433257844417962 + }, + "course_id": "DS4400", + "delay": { + "max": 6.0, + "mean": 5.725637808448341, + "median": 5.725637808448341, + "min": 4.0, + "q1": 5.265145826010389, + "q3": 6.0, + "std_dev": 0.46049198243795125 + }, + "plan_count": 4782 + }, + "name": "Machine Learning and Data Mining 1", + "number": "4400", + "prefix": "DS", + "prerequisites": { + "and": [ + { + "or": [ + "CS2810", + "DS3000" + ] + }, + { + "or": [ + "CS3100", + "CS3500" + ] + } + ] + }, + "strict_corequisites": [], + "tags": [ + "ai" + ] + }, + "DS4420": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 5.944540727902941, + "median": 6.0, + "min": 5.0, + "q1": 6.0, + "q3": 6.0, + "std_dev": 0.22887450979853294 + }, + "course_id": "DS4420", + "delay": { + "max": 6.0, + "mean": 5.944540727902941, + "median": 5.944540727902941, + "min": 5.0, + "q1": 5.715666218104409, + "q3": 6.0, + "std_dev": 0.22887450979853294 + }, + "plan_count": 1731 + }, + "name": "Machine Learning and Data Mining 2", + "number": "4420", + "prefix": "DS", + "prerequisites": "DS4400", + "strict_corequisites": [], + "tags": [ + "ai" + ] + }, + "DS4440": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 5.966254218222725, + "median": 6.0, + "min": 5.0, + "q1": 6.0, + "q3": 6.0, + "std_dev": 0.18057409556610868 + }, + "course_id": "DS4440", + "delay": { + "max": 6.0, + "mean": 5.966254218222725, + "median": 5.966254218222725, + "min": 5.0, + "q1": 5.785680122656617, + "q3": 6.0, + "std_dev": 0.18057409556610868 + }, + "plan_count": 1778 + }, + "name": "Practical Neural Networks", + "number": "4440", + "prefix": "DS", + "prerequisites": "DS4400", + "strict_corequisites": [], + "tags": [ + "ai" + ] + }, + "EECE2310": { + "canonical_name": null, + "corequisites": [ + "EECE2311" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "EECE2310", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 9983 + }, + "name": "Introduction to Digital Design and Computer Architecture", + "number": "2310", + "prefix": "EECE", + "strict_corequisites": [ + "EECE2311" + ] + }, + "EECE2311": { + "canonical_name": null, + "corequisites": [ + "EECE2310" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "EECE2311", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 9983 + }, + "name": "Lab for EECE 2310", + "number": "2311", + "prefix": "EECE", + "strict_corequisites": [ + "EECE2310" + ] + }, + "ENGW1111": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "First-Year Writing", + "number": "1111", + "prefix": "ENGW", + "strict_corequisites": [] + }, + "ENGW3302": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Advanced Writing in the Technical Professions", + "number": "3302", + "prefix": "ENGW", + "prerequisites": "ENGW1111", + "strict_corequisites": [] + }, + "ENGW3315": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Interdisciplinary Advanced Writing in the Disciplines", + "number": "3315", + "prefix": "ENGW", + "prerequisites": "ENGW1111", + "strict_corequisites": [] + }, + "ENVR1200": { + "canonical_name": null, + "corequisites": [ + "ENVR1201" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Dynamic Earth", + "number": "1200", + "prefix": "ENVR", + "strict_corequisites": [ + "ENVR1201" + ] + }, + "ENVR1201": { + "canonical_name": null, + "corequisites": [ + "ENVR1200" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "name": "Lab for ENVR 1200", + "number": "1201", + "prefix": "ENVR", + "strict_corequisites": [ + "ENVR1200" + ] + }, + "ENVR2310": { + "canonical_name": null, + "corequisites": [ + "ENVR2311" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Earth Materials", + "number": "2310", + "prefix": "ENVR", + "strict_corequisites": [ + "ENVR2311" + ] + }, + "ENVR2311": { + "canonical_name": null, + "corequisites": [ + "ENVR2310" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "name": "Lab for ENVR 2310", + "number": "2311", + "prefix": "ENVR", + "strict_corequisites": [ + "ENVR2310" + ] + }, + "ENVR2340": { + "canonical_name": null, + "corequisites": [ + "ENVR2341" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Earth Landforms and Processes", + "number": "2340", + "prefix": "ENVR", + "strict_corequisites": [ + "ENVR2341" + ] + }, + "ENVR2341": { + "canonical_name": null, + "corequisites": [ + "ENVR2340" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "name": "Lab for ENVR 2340", + "number": "2341", + "prefix": "ENVR", + "strict_corequisites": [ + "ENVR2340" + ] + }, + "ENVR3300": { + "canonical_name": null, + "corequisites": [ + "ENVR3301" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Geographic Information Systems", + "number": "3300", + "prefix": "ENVR", + "strict_corequisites": [ + "ENVR3301" + ] + }, + "ENVR3301": { + "canonical_name": null, + "corequisites": [ + "ENVR3300" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "name": "Lab for ENVR 3300", + "number": "3301", + "prefix": "ENVR", + "strict_corequisites": [ + "ENVR3300" + ] + }, + "ENVR4500": { + "canonical_name": null, + "corequisites": [ + "ENVR4501" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Applied Hydrogeology", + "number": "4500", + "prefix": "ENVR", + "strict_corequisites": [ + "ENVR4501" + ] + }, + "ENVR4501": { + "canonical_name": null, + "corequisites": [ + "ENVR4500" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "name": "Lab for ENVR 4500", + "number": "4501", + "prefix": "ENVR", + "strict_corequisites": [ + "ENVR4500" + ] + }, + "HINF5300": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "HINF5300", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 330 + }, + "name": "Personal Health Interface Design and Development", + "number": "5300", + "prefix": "HINF", + "strict_corequisites": [] + }, + "HIST2220": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "History of Technology", + "number": "2220", + "prefix": "HIST", + "strict_corequisites": [] + }, + "INSH2102": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Bostonography", + "number": "2102", + "prefix": "INSH", + "strict_corequisites": [] + }, + "JRNL3700": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Data Storytelling", + "number": "3700", + "prefix": "JRNL", + "strict_corequisites": [] + }, + "MATH1341": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 10.0, + "mean": 4.75448262045479, + "median": 4.75448262045479, + "min": 2.0, + "q1": 3.2482354238356828, + "q3": 6.260729817073898, + "std_dev": 1.5062471966191073 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 16.0, + "mean": 9.809876790543873, + "median": 10.0, + "min": 5.0, + "q1": 9.0, + "q3": 11.0, + "std_dev": 2.2786270548246117 + }, + "course_id": "MATH1341", + "delay": { + "max": 6.0, + "mean": 5.055394170089154, + "median": 5.055394170089154, + "min": 3.0, + "q1": 4.205414821686099, + "q3": 5.905373518492209, + "std_dev": 0.8499793484030549 + }, + "plan_count": 9983 + }, + "name": "Calculus 1 for Science and Engineering", + "number": "1341", + "prefix": "MATH", + "strict_corequisites": [] + }, + "MATH1342": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 9.0, + "mean": 3.754482620454765, + "median": 3.754482620454765, + "min": 1.0, + "q1": 2.2482354238356574, + "q3": 5.260729817073873, + "std_dev": 1.5062471966191078 + }, + "centrality": { + "max": 33.0, + "mean": 8.630672142642462, + "median": 8.630672142642462, + "min": 3.0, + "q1": 3.572765566678923, + "q3": 13.688578718606001, + "std_dev": 5.057906575963539 + }, + "complexity": { + "max": 15.0, + "mean": 8.809876790543873, + "median": 9.0, + "min": 4.0, + "q1": 8.0, + "q3": 11.0, + "std_dev": 2.2786270548246117 + }, + "course_id": "MATH1342", + "delay": { + "max": 6.0, + "mean": 5.055394170089154, + "median": 5.055394170089154, + "min": 3.0, + "q1": 4.205414821686099, + "q3": 5.905373518492209, + "std_dev": 0.8499793484030549 + }, + "plan_count": 9983 + }, + "name": "Calculus 2 for Science and Engineering", + "number": "1342", + "prefix": "MATH", + "prerequisites": "MATH1341", + "strict_corequisites": [] + }, + "MATH1365": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "MATH1365", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 9983 + }, + "name": "Introduction to Mathematical Reasoning", + "number": "1365", + "prefix": "MATH", + "strict_corequisites": [] + }, + "MATH1465": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Intensive Mathematical Reasoning", + "number": "1465", + "prefix": "MATH", + "strict_corequisites": [] + }, + "MATH2280": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 8.0, + "mean": 2.754482620454765, + "median": 2.754482620454765, + "min": 0.0, + "q1": 1.2482354238356574, + "q3": 4.260729817073873, + "std_dev": 1.5062471966191078 + }, + "centrality": { + "max": 33.0, + "mean": 8.4425523389763, + "median": 8.4425523389763, + "min": 0.0, + "q1": 3.1293733322414328, + "q3": 13.755731345711169, + "std_dev": 5.313179006734868 + }, + "complexity": { + "max": 14.0, + "mean": 7.809876790543939, + "median": 8.0, + "min": 3.0, + "q1": 7.0, + "q3": 10.0, + "std_dev": 2.278627054824612 + }, + "course_id": "MATH2280", + "delay": { + "max": 6.0, + "mean": 5.055394170089154, + "median": 5.055394170089154, + "min": 3.0, + "q1": 4.205414821686099, + "q3": 5.905373518492209, + "std_dev": 0.8499793484030549 + }, + "plan_count": 9983 + }, + "name": "Statistics and Software", + "number": "2280", + "prefix": "MATH", + "prerequisites": "MATH1342", + "strict_corequisites": [] + }, + "MATH2310": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Mathematical Reasoning", + "number": "2310", + "prefix": "MATH", + "strict_corequisites": [] + }, + "MATH2331": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Linear Algebra", + "number": "2331", + "prefix": "MATH", + "prerequisites": "MATH1342", + "strict_corequisites": [] + }, + "MATH3081": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Probability and Statistics", + "number": "3081", + "prefix": "MATH", + "prerequisites": "MATH1342", + "strict_corequisites": [] + }, + "MKTG4606": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "MKTG4606", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 1290 + }, + "name": "Digital Marketing and Web Analytics", + "number": "4606", + "prefix": "MKTG", + "strict_corequisites": [] + }, + "PHIL1145": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Technology and Human Values", + "number": "1145", + "prefix": "PHIL", + "strict_corequisites": [] + }, + "PHIL1300": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "cross_listed_as": [ + "DS1300" + ], + "csv_id": null, + "id": null, + "name": "Knowledge in a Digital World", + "number": "1300", + "prefix": "PHIL", + "strict_corequisites": [] + }, + "PHYS1145": { + "canonical_name": null, + "corequisites": [ + "PHYS1146" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Physics for Life Sciences 1", + "number": "1145", + "prefix": "PHYS", + "strict_corequisites": [ + "PHYS1146" + ] + }, + "PHYS1146": { + "canonical_name": null, + "corequisites": [ + "PHYS1145" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "name": "Lab for PHYS 1145", + "number": "1146", + "prefix": "PHYS", + "strict_corequisites": [ + "PHYS1145" + ] + }, + "PHYS1147": { + "canonical_name": null, + "corequisites": [ + "PHYS1148" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Physics for Life Sciences 2", + "number": "1147", + "prefix": "PHYS", + "prerequisites": "PHYS1145", + "strict_corequisites": [ + "PHYS1148" + ] + }, + "PHYS1148": { + "canonical_name": null, + "corequisites": [ + "PHYS1147" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "name": "Lab for PHYS 1147", + "number": "1148", + "prefix": "PHYS", + "strict_corequisites": [ + "PHYS1147" + ] + }, + "PHYS1151": { + "canonical_name": null, + "corequisites": [ + "PHYS1152" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Physics for Engineering 1", + "number": "1151", + "prefix": "PHYS", + "prerequisites": "MATH1341", + "strict_corequisites": [ + "PHYS1152" + ] + }, + "PHYS1152": { + "canonical_name": null, + "corequisites": [ + "PHYS1151" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "name": "Lab for PHYS 1151", + "number": "1152", + "prefix": "PHYS", + "strict_corequisites": [ + "PHYS1151" + ] + }, + "PHYS1155": { + "canonical_name": null, + "corequisites": [ + "PHYS1156" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Physics for Engineering 2", + "number": "1155", + "prefix": "PHYS", + "prerequisites": "PHYS1151", + "strict_corequisites": [ + "PHYS1156" + ] + }, + "PHYS1156": { + "canonical_name": null, + "corequisites": [ + "PHYS1155" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "name": "Lab for PHYS 1155", + "number": "1156", + "prefix": "PHYS", + "strict_corequisites": [ + "PHYS1155" + ] + }, + "PHYS1161": { + "canonical_name": null, + "corequisites": [ + "PHYS1162" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Physics 1", + "number": "1161", + "prefix": "PHYS", + "strict_corequisites": [ + "PHYS1162" + ] + }, + "PHYS1162": { + "canonical_name": null, + "corequisites": [ + "PHYS1161" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "name": "Lab for PHYS 1161", + "number": "1162", + "prefix": "PHYS", + "strict_corequisites": [ + "PHYS1161" + ] + }, + "PHYS1165": { + "canonical_name": null, + "corequisites": [ + "PHYS1166" + ], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Physics 2", + "number": "1165", + "prefix": "PHYS", + "prerequisites": "PHYS1161", + "strict_corequisites": [ + "PHYS1166" + ] + }, + "PHYS1166": { + "canonical_name": null, + "corequisites": [ + "PHYS1165" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "name": "Lab for PHYS 1165", + "number": "1166", + "prefix": "PHYS", + "strict_corequisites": [ + "PHYS1165" + ] + }, + "SOCL1280": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "The Twenty-First-Century Workplace", + "number": "1280", + "prefix": "SOCL", + "strict_corequisites": [] + }, + "SOCL2485": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Environment, Technology, and Society", + "number": "2485", + "prefix": "SOCL", + "strict_corequisites": [] + }, + "SOCL4528": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Technology and Society", + "number": "4528", + "prefix": "SOCL", + "strict_corequisites": [] + }, + "THTR1125": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "THTR1125", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 1254 + }, + "name": "Improvisation", + "number": "1125", + "prefix": "THTR", + "strict_corequisites": [] + }, + "THTR1130": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "THTR1130", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 1263 + }, + "name": "Introduction to Acting", + "number": "1130", + "prefix": "THTR", + "strict_corequisites": [] + }, + "THTR1180": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "THTR1180", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 1245 + }, + "name": "Dynamic Presence: Theatre Training for Effective Interpersonal Interactions", + "number": "1180", + "prefix": "THTR", + "strict_corequisites": [] + }, + "THTR2345": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "THTR2345", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 1234 + }, + "name": "Acting for the Camera", + "number": "2345", + "prefix": "THTR", + "strict_corequisites": [] + } + } +} diff --git a/samples/degrees/uhm-ics-bscs-general_report.json b/samples/degrees/uhm-ics-bscs-general_report.json new file mode 100644 index 0000000..4642088 --- /dev/null +++ b/samples/degrees/uhm-ics-bscs-general_report.json @@ -0,0 +1,5488 @@ +{ + "degree": { + "allow_double_counting": false, + "catalog_year": "2024-2025", + "cip_code": null, + "degree_type": "BS", + "gpa_minimum": 2.0, + "grade_minimum": "C", + "grade_minimum_note": "C means C (2.0), not C- (1.7). Grade of B required in ICS 111 for progression to ICS 211.", + "id": "university-of-hawaii-at-manoa-bachelor-of-science-in-computer-science", + "in_major_credits": 57, + "institution": "University of Hawaiʻi at Mānoa", + "major_subjects": [ + "ICS" + ], + "name": "Bachelor of Science in Computer Science - General Track", + "source_url": "https://manoa.hawaii.edu/catalog/schools-colleges/nat-sci/ics/", + "system_type": "semester", + "total_credits": 120, + "upper_division_credits": 45 + }, + "analysis": { + "metrics": { + "complexity": { + "max": 349.0, + "mean": 298.81580271766467, + "median": 298.0, + "min": 250.0, + "q1": 286.75, + "q3": 310.0, + "std_dev": 17.117773217826056 + }, + "credits": { + "max": 143.0, + "mean": 132.00251635631597, + "median": 132.00251635631597, + "min": 125.0, + "q1": 128.9556184215682, + "q3": 135.04941429106373, + "std_dev": 3.0468979347477685 + }, + "delay": { + "max": 6.0, + "mean": 5.490689481630587, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 6.0, + "std_dev": 0.4999133067319703 + } + }, + "sample_type": "shuffled", + "variations_run": 7948 + }, + "requirements": { + "breadth_requirements": { + "category": "major", + "from": { + "groups": [ + { + "courses": [ + "ICS312", + "ICS331" + ], + "id": "systems_hardware", + "name": "Systems and Hardware" + }, + { + "courses": [ + "ICS313", + "ICS361" + ], + "id": "languages_ai", + "name": "Languages and AI" + }, + { + "courses": [ + "ICS351", + "ICS451" + ], + "id": "networking", + "name": "Networking" + } + ], + "groups_required": 2, + "per_group": 1 + }, + "name": "Breadth Requirements (Two of Three Areas)", + "type": "select" + }, + "calculus": { + "category": "supporting", + "courses": [ + "{MATH215, MATH241, MATH251A}", + "{MATH216, MATH242, MATH252A}" + ], + "name": "Calculus Sequence", + "type": "all" + }, + "capstone": { + "category": "major", + "courses": [ + "ICS496" + ], + "name": "Capstone Project", + "type": "all" + }, + "core_courses": { + "category": "major", + "courses": [ + "ICS311", + "ICS314", + "ICS321", + "ICS332", + "ICS355" + ], + "name": "Computer Science Core", + "type": "all" + }, + "discrete_math": { + "category": "major", + "name": "Discrete Mathematics Requirement", + "options": [ + { + "id": "ics_path", + "name": "ICS Discrete Math Sequence", + "requirements": [ + { + "courses": [ + "ICS141", + "ICS241" + ], + "type": "all" + } + ] + }, + { + "id": "math_substitution", + "name": "Math Department Substitution", + "requirements": [ + { + "courses": [ + "MATH301", + "MATH372" + ], + "type": "all" + } + ] + } + ], + "type": "one_of" + }, + "free_electives": { + "category": "elective", + "credits": 6, + "from": { + "pattern": "*:*" + }, + "name": "Free Electives", + "type": "select" + }, + "gen_ed_arts_humanities": { + "category": "gen_ed", + "credits": 6, + "from": { + "pattern": "*:*" + }, + "name": "Arts, Humanities, and Literature (DA/DH/DL)", + "type": "select" + }, + "gen_ed_biological": { + "category": "gen_ed", + "credits": 3, + "from": { + "pattern": "*:*" + }, + "name": "Biological Sciences (DB)", + "type": "select" + }, + "gen_ed_global": { + "category": "gen_ed", + "credits": 6, + "from": { + "pattern": "*:*" + }, + "name": "Global and Multicultural Perspectives (FG)", + "type": "select" + }, + "gen_ed_quantitative": { + "category": "gen_ed", + "count": 1, + "from": { + "courses": [ + "MATH215", + "MATH241", + "MATH251A", + "ICS141" + ] + }, + "name": "Quantitative Reasoning (FQ)", + "type": "select" + }, + "gen_ed_social_sciences": { + "category": "gen_ed", + "credits": 6, + "from": { + "pattern": "*:*" + }, + "name": "Social Sciences (DS)", + "type": "select" + }, + "gen_ed_written": { + "category": "gen_ed", + "count": 1, + "from": { + "courses": [ + "ENG100" + ] + }, + "name": "Written Communication (FW)", + "type": "select" + }, + "intro_sequence": { + "category": "major", + "constraints": { + "grade_minimum": "B" + }, + "courses": [ + "ICS111", + "ICS211" + ], + "name": "Introductory Computer Science Sequence", + "type": "all" + }, + "math_elective": { + "category": "major", + "count": 1, + "from": { + "courses": [ + "MATH307", + "MATH372" + ] + }, + "name": "Mathematics or Statistics Elective", + "type": "select" + }, + "program_structure": { + "category": "major", + "courses": [ + "ICS212" + ], + "name": "Program Structure", + "type": "all" + }, + "science_sequence": { + "category": "supporting", + "name": "Natural Science Sequence with Labs", + "options": [ + { + "id": "physics_calc", + "name": "Physics (Calculus-Based)", + "requirements": [ + { + "courses": [ + "[PHYS170, PHYS170L]", + "[PHYS272, PHYS272L]" + ], + "type": "all" + } + ] + }, + { + "id": "physics_algebra", + "name": "Physics (Algebra-Based)", + "requirements": [ + { + "courses": [ + "[PHYS151, PHYS151L]", + "[PHYS152, PHYS152L]" + ], + "type": "all" + } + ] + }, + { + "id": "chemistry", + "name": "Chemistry", + "requirements": [ + { + "courses": [ + "[CHEM161, CHEM161L]", + "[CHEM162, CHEM162L]" + ], + "type": "all" + } + ] + } + ], + "type": "one_of" + }, + "second_language": { + "category": "gen_ed", + "credits": 12, + "from": { + "pattern": "*:*" + }, + "name": "Hawaiian/Second Language (HSL)", + "type": "select" + }, + "upper_electives": { + "category": "major", + "constraints": { + "exclude_used": true + }, + "count": 4, + "from": { + "exclude": [ + "ICS496" + ], + "pattern": "ICS:400+" + }, + "name": "400-Level Electives", + "type": "select" + } + }, + "selected_plans": [ + { + "category": "Shortest Path", + "course_count": 42, + "credits": 125.0, + "critical_path": [ + "MATH215", + "MATH216", + "MATH372", + "ICS355", + "ICS426" + ], + "is_calc_ready": false, + "longest_delay": 5, + "schedule": [ + { + "courses": [ + "MATH215", + "ICS111", + "GEB01", + "GESS02", + "SL04" + ], + "credits": 17.0, + "term": 1 + }, + { + "courses": [ + "ICS141", + "MATH216", + "ICS211", + "PHYS170", + "PHYS170L" + ], + "credits": 15.0, + "term": 2 + }, + { + "courses": [ + "ICS241", + "MATH372", + "ICS212", + "PHYS272", + "PHYS272L", + "ICS499", + "SL01" + ], + "credits": 16.0, + "term": 3 + }, + { + "courses": [ + "ICS355", + "ICS311", + "ICS314", + "GESS01", + "SL02" + ], + "credits": 16.0, + "term": 4 + }, + { + "courses": [ + "ICS312", + "ICS313", + "ICS321", + "ICS331", + "SL03" + ], + "credits": 16.0, + "term": 5 + }, + { + "courses": [ + "ICS332", + "ICS351", + "ICS361", + "ICS426", + "ICS451" + ], + "credits": 15.0, + "term": 6 + }, + { + "courses": [ + "ICS496", + "FE01", + "GEAH01", + "GEG01", + "ICS452" + ], + "credits": 15.0, + "term": 7 + }, + { + "courses": [ + "ENG100", + "FE02", + "GEAH02", + "GEG02", + "ICS495" + ], + "credits": 15.0, + "term": 8 + } + ], + "terms_required": 8, + "total_complexity": 250 + }, + { + "category": "Longest Path", + "course_count": 45, + "credits": 137.0, + "critical_path": [ + "MATH215", + "MATH216", + "MATH372", + "ICS314", + "ICS481", + "ICS488" + ], + "is_calc_ready": false, + "longest_delay": 6, + "schedule": [ + { + "courses": [ + "MATH215", + "ICS111", + "FE02", + "GEAH02", + "GESS02" + ], + "credits": 17.0, + "term": 1 + }, + { + "courses": [ + "ICS141", + "MATH216", + "ICS211", + "MATH301", + "GEB01", + "SL04" + ], + "credits": 19.0, + "term": 2 + }, + { + "courses": [ + "ICS241", + "MATH372", + "ICS212", + "PHYS170", + "PHYS170L", + "SL01" + ], + "credits": 17.0, + "term": 3 + }, + { + "courses": [ + "ICS311", + "ICS314", + "ICS355", + "MATH307", + "GEG01" + ], + "credits": 16.0, + "term": 4 + }, + { + "courses": [ + "ICS321", + "ICS331", + "ICS481", + "ICS312", + "GEG02" + ], + "credits": 16.0, + "term": 5 + }, + { + "courses": [ + "ICS431", + "ICS438", + "ICS488", + "ICS313", + "ICS332", + "SL02" + ], + "credits": 18.0, + "term": 6 + }, + { + "courses": [ + "ICS351", + "ICS361", + "ICS415", + "ICS451", + "ICS496", + "SL03" + ], + "credits": 18.0, + "term": 7 + }, + { + "courses": [ + "PHYS272", + "PHYS272L", + "ENG100", + "FE01", + "GEAH01", + "GESS01" + ], + "credits": 16.0, + "term": 8 + } + ], + "terms_required": 8, + "total_complexity": 349 + }, + { + "category": "Random Sample", + "course_count": 43, + "credits": 131.0, + "critical_path": [ + "MATH215", + "MATH216", + "MATH372", + "ICS314", + "ICS496" + ], + "is_calc_ready": false, + "longest_delay": 5, + "schedule": [ + { + "courses": [ + "MATH215", + "ICS111", + "GEAH02", + "GEG02", + "SL01" + ], + "credits": 17.0, + "term": 1 + }, + { + "courses": [ + "ICS141", + "MATH216", + "ICS211", + "MATH301", + "ICS452" + ], + "credits": 16.0, + "term": 2 + }, + { + "courses": [ + "ICS241", + "MATH372", + "ICS212", + "PHYS170", + "PHYS170L", + "SL02" + ], + "credits": 17.0, + "term": 3 + }, + { + "courses": [ + "ICS311", + "ICS314", + "ICS355", + "PHYS272", + "PHYS272L", + "SL03" + ], + "credits": 17.0, + "term": 4 + }, + { + "courses": [ + "ICS312", + "ICS313", + "ICS321", + "ICS331", + "ICS491" + ], + "credits": 16.0, + "term": 5 + }, + { + "courses": [ + "ICS332", + "ICS351", + "ICS361", + "ICS451", + "ICS469", + "SL04" + ], + "credits": 18.0, + "term": 6 + }, + { + "courses": [ + "ICS485", + "ICS496", + "FE02", + "GEB01", + "GESS01" + ], + "credits": 15.0, + "term": 7 + }, + { + "courses": [ + "ENG100", + "FE01", + "GEAH01", + "GEG01", + "GESS02" + ], + "credits": 15.0, + "term": 8 + } + ], + "terms_required": 8, + "total_complexity": 280 + }, + { + "category": "Random Sample", + "course_count": 43, + "credits": 131.0, + "critical_path": [ + "MATH215", + "MATH216", + "MATH372", + "ICS314", + "ICS496" + ], + "is_calc_ready": false, + "longest_delay": 5, + "schedule": [ + { + "courses": [ + "MATH215", + "ICS111", + "GEAH02", + "GEG02", + "SL02" + ], + "credits": 17.0, + "term": 1 + }, + { + "courses": [ + "ICS141", + "MATH216", + "ICS211", + "MATH301", + "GESS02" + ], + "credits": 16.0, + "term": 2 + }, + { + "courses": [ + "ICS241", + "MATH372", + "ICS212", + "PHYS170", + "PHYS170L", + "SL03" + ], + "credits": 17.0, + "term": 3 + }, + { + "courses": [ + "ICS311", + "ICS314", + "ICS355", + "ICS434", + "ICS452" + ], + "credits": 16.0, + "term": 4 + }, + { + "courses": [ + "ICS312", + "ICS313", + "ICS321", + "ICS331", + "ICS495" + ], + "credits": 16.0, + "term": 5 + }, + { + "courses": [ + "ICS332", + "ICS351", + "ICS361", + "ICS424", + "ICS451", + "SL04" + ], + "credits": 18.0, + "term": 6 + }, + { + "courses": [ + "ICS496", + "PHYS272", + "PHYS272L", + "GEAH01", + "GEG01", + "SL01" + ], + "credits": 16.0, + "term": 7 + }, + { + "courses": [ + "ENG100", + "FE01", + "FE02", + "GEB01", + "GESS01" + ], + "credits": 15.0, + "term": 8 + } + ], + "terms_required": 8, + "total_complexity": 270 + }, + { + "category": "Random Sample", + "course_count": 44, + "credits": 134.0, + "critical_path": [ + "MATH215", + "MATH216", + "MATH372", + "ICS314", + "ICS432" + ], + "is_calc_ready": false, + "longest_delay": 6, + "schedule": [ + { + "courses": [ + "MATH215", + "ICS111", + "GEAH01", + "GEB01", + "SL01" + ], + "credits": 17.0, + "term": 1 + }, + { + "courses": [ + "ICS141", + "MATH216", + "ICS211", + "MATH301", + "GEG02" + ], + "credits": 16.0, + "term": 2 + }, + { + "courses": [ + "ICS241", + "MATH372", + "ICS212", + "PHYS170", + "PHYS170L", + "SL02" + ], + "credits": 17.0, + "term": 3 + }, + { + "courses": [ + "ICS311", + "ICS314", + "ICS355", + "MATH307", + "GESS01" + ], + "credits": 16.0, + "term": 4 + }, + { + "courses": [ + "ICS332", + "ICS312", + "ICS313", + "ICS321", + "ICS351", + "SL03" + ], + "credits": 18.0, + "term": 5 + }, + { + "courses": [ + "ICS432", + "ICS331", + "ICS361", + "ICS451", + "GESS02" + ], + "credits": 16.0, + "term": 6 + }, + { + "courses": [ + "ICS461", + "ICS462", + "ICS496", + "PHYS272", + "PHYS272L", + "ICS452" + ], + "credits": 16.0, + "term": 7 + }, + { + "courses": [ + "ENG100", + "FE01", + "FE02", + "GEAH02", + "GEG01", + "SL04" + ], + "credits": 18.0, + "term": 8 + } + ], + "terms_required": 8, + "total_complexity": 314 + }, + { + "category": "Random Sample", + "course_count": 43, + "credits": 131.0, + "critical_path": [ + "MATH215", + "MATH216", + "MATH372", + "ICS355", + "ICS455" + ], + "is_calc_ready": false, + "longest_delay": 5, + "schedule": [ + { + "courses": [ + "MATH215", + "ICS111", + "GEAH02", + "GEG02", + "SL01" + ], + "credits": 17.0, + "term": 1 + }, + { + "courses": [ + "ICS141", + "MATH216", + "ICS211", + "PHYS170", + "PHYS170L", + "SL03" + ], + "credits": 18.0, + "term": 2 + }, + { + "courses": [ + "ICS241", + "MATH372", + "ICS212", + "ICS434", + "MATH307", + "SL04" + ], + "credits": 18.0, + "term": 3 + }, + { + "courses": [ + "ICS355", + "ICS311", + "ICS314", + "PHYS272", + "PHYS272L", + "SL02" + ], + "credits": 17.0, + "term": 4 + }, + { + "courses": [ + "ICS312", + "ICS313", + "ICS321", + "ICS331", + "ICS452" + ], + "credits": 16.0, + "term": 5 + }, + { + "courses": [ + "ICS332", + "ICS351", + "ICS361", + "ICS451", + "ICS455" + ], + "credits": 15.0, + "term": 6 + }, + { + "courses": [ + "ICS461", + "ICS496", + "FE02", + "GEB01", + "GESS01" + ], + "credits": 15.0, + "term": 7 + }, + { + "courses": [ + "ENG100", + "FE01", + "GEAH01", + "GEG01", + "GESS02" + ], + "credits": 15.0, + "term": 8 + } + ], + "terms_required": 8, + "total_complexity": 271 + }, + { + "category": "Random Sample", + "course_count": 43, + "credits": 131.0, + "critical_path": [ + "MATH215", + "MATH216", + "MATH372", + "ICS314", + "ICS321", + "ICS421" + ], + "is_calc_ready": false, + "longest_delay": 6, + "schedule": [ + { + "courses": [ + "MATH215", + "ICS111", + "GEAH01", + "GEB01", + "SL01" + ], + "credits": 17.0, + "term": 1 + }, + { + "courses": [ + "ICS141", + "MATH216", + "ICS211", + "MATH301", + "GESS01" + ], + "credits": 16.0, + "term": 2 + }, + { + "courses": [ + "ICS241", + "MATH372", + "ICS212", + "PHYS170", + "PHYS170L", + "SL02" + ], + "credits": 17.0, + "term": 3 + }, + { + "courses": [ + "ICS311", + "ICS314", + "ICS355", + "PHYS272", + "PHYS272L", + "SL03" + ], + "credits": 17.0, + "term": 4 + }, + { + "courses": [ + "ICS321", + "ICS312", + "ICS313", + "ICS331", + "GESS02" + ], + "credits": 16.0, + "term": 5 + }, + { + "courses": [ + "ICS421", + "ICS332", + "ICS351", + "ICS361", + "ICS422", + "SL04" + ], + "credits": 18.0, + "term": 6 + }, + { + "courses": [ + "ICS425", + "ICS451", + "ICS469", + "ICS496", + "GEG01" + ], + "credits": 15.0, + "term": 7 + }, + { + "courses": [ + "ENG100", + "FE01", + "FE02", + "GEAH02", + "GEG02" + ], + "credits": 15.0, + "term": 8 + } + ], + "terms_required": 8, + "total_complexity": 316 + } + ], + "courses": { + "CHEM161": { + "canonical_name": null, + "corequisites": [ + "CHEM161L" + ], + "credit_hours": 3.0, + "csv_id": null, + "gen_ed_attributes": [ + "DP" + ], + "id": null, + "name": "General Chemistry I", + "number": "161", + "prefix": "CHEM", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "CHEM161L": { + "canonical_name": null, + "corequisites": [ + "CHEM161" + ], + "credit_hours": 1.0, + "csv_id": null, + "gen_ed_attributes": [ + "DY" + ], + "id": null, + "name": "General Chemistry I Lab", + "number": "161L", + "prefix": "CHEM", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "CHEM162": { + "canonical_name": null, + "corequisites": [ + "CHEM162L" + ], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "name": "General Chemistry II", + "number": "162", + "prefix": "CHEM", + "prerequisites": "CHEM161", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "CHEM162L": { + "canonical_name": null, + "corequisites": [ + "CHEM162" + ], + "credit_hours": 1.0, + "csv_id": null, + "id": null, + "name": "General Chemistry II Lab", + "number": "162L", + "prefix": "CHEM", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "DATA235": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "cross_listed_as": [ + "ICS235" + ], + "csv_id": null, + "id": null, + "name": "Machine Learning Methods", + "number": "235", + "prefix": "DATA", + "prerequisites": { + "or": [ + "MATH241", + "MATH203", + "MATH215", + "MATH251A" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall" + ] + }, + "DATA422": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "cross_listed_as": [ + "ICS422" + ], + "csv_id": null, + "id": null, + "name": "Data Processing", + "number": "422", + "prefix": "DATA", + "prerequisites": { + "or": [ + "ICS311", + "ECE367" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "spring" + ] + }, + "DATA434": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "cross_listed_as": [ + "ICS434" + ], + "csv_id": null, + "id": null, + "name": "Data Science Fundamentals", + "number": "434", + "prefix": "DATA", + "prerequisites": "ICS211", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "DATA435": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "cross_listed_as": [ + "ICS435" + ], + "csv_id": null, + "id": null, + "name": "Machine Learning Fundamentals", + "number": "435", + "prefix": "DATA", + "prerequisites": "ICS235", + "strict_corequisites": [], + "typically_offered": [ + "fall" + ] + }, + "DATA438": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "cross_listed_as": [ + "ICS438" + ], + "csv_id": null, + "id": null, + "name": "Big Data Analytics", + "number": "438", + "prefix": "DATA", + "prerequisites": "ICS321", + "strict_corequisites": [], + "typically_offered": [ + "spring" + ] + }, + "DATA484": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "cross_listed_as": [ + "ICS484", + "CINE484" + ], + "csv_id": null, + "id": null, + "name": "Data Visualization", + "number": "484", + "prefix": "DATA", + "prerequisites": { + "and": [ + { + "or": [ + "ICS311", + "ECE367" + ] + }, + "ICS314" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "spring" + ] + }, + "ECE160": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Programming for Engineers", + "number": "160", + "prefix": "ECE", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ECE205": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "name": "Object-Oriented Programming", + "number": "205", + "prefix": "ECE", + "prerequisites": "ECE160", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ECE260": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Introduction to Circuits", + "number": "260", + "prefix": "ECE", + "prerequisites": { + "and": [ + "MATH242", + "PHYS272" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ECE361": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "name": "Digital Systems and Computer Design", + "number": "361", + "prefix": "ECE", + "prerequisites": { + "and": [ + "ECE205", + "ECE260" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ECE362": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "name": "Discrete Mathematics for Computer Engineers", + "number": "362", + "prefix": "ECE", + "prerequisites": { + "and": [ + "ECE160", + "ECE260", + "MATH242" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ECE367": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "name": "Algorithms", + "number": "367", + "prefix": "ECE", + "prerequisites": { + "and": [ + { + "or": [ + "ECE205", + "ICS212" + ] + }, + { + "or": [ + "ECE362", + "ICS241" + ] + } + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ENG100": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "gen_ed_attributes": [ + "FW" + ], + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "ENG100", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 7948 + }, + "name": "Composition I", + "number": "100", + "prefix": "ENG", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS110D": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "name": "Introduction to Programming Through Animations", + "number": "110D", + "prefix": "ICS", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS111": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 19.0, + "mean": 15.919979869149456, + "median": 15.919979869149456, + "min": 13.0, + "q1": 14.839830762218748, + "q3": 17.000128976080163, + "std_dev": 1.0801491069307083 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 24.0, + "mean": 20.410669350780026, + "median": 20.0, + "min": 17.0, + "q1": 19.0, + "q3": 21.0, + "std_dev": 1.33718710571623 + }, + "course_id": "ICS111", + "delay": { + "max": 5.0, + "mean": 4.490689481630587, + "median": 4.490689481630587, + "min": 4.0, + "q1": 4.0, + "q3": 4.9906027883625566, + "std_dev": 0.4999133067319703 + }, + "plan_count": 7948 + }, + "name": "Introduction to Computer Science I", + "number": "111", + "prefix": "ICS", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS141": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "gen_ed_attributes": [ + "FQ" + ], + "id": null, + "metrics": { + "blocking": { + "max": 19.0, + "mean": 15.01862103673882, + "median": 15.01862103673882, + "min": 11.0, + "q1": 13.417270477269291, + "q3": 16.619971596208348, + "std_dev": 1.601350559469529 + }, + "centrality": { + "max": 138.0, + "mean": 86.41545042778067, + "median": 86.41545042778067, + "min": 45.0, + "q1": 55.70707533577885, + "q3": 117.12382551978249, + "std_dev": 30.708375092001816 + }, + "complexity": { + "max": 25.0, + "mean": 20.509310518369297, + "median": 21.0, + "min": 16.0, + "q1": 19.0, + "q3": 22.0, + "std_dev": 1.7676534642593809 + }, + "course_id": "ICS141", + "delay": { + "max": 6.0, + "mean": 5.490689481630587, + "median": 5.490689481630587, + "min": 5.0, + "q1": 5.0, + "q3": 5.9906027883625566, + "std_dev": 0.4999133067319703 + }, + "plan_count": 7948 + }, + "name": "Discrete Mathematics for Computer Science I", + "number": "141", + "prefix": "ICS", + "prerequisites": { + "or": [ + "MATH215", + "MATH241", + "MATH251A" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS211": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 17.0, + "mean": 14.677403120281793, + "median": 14.677403120281793, + "min": 12.0, + "q1": 13.663107864114354, + "q3": 15.691698376449233, + "std_dev": 1.01429525616744 + }, + "centrality": { + "max": 134.0, + "mean": 110.39003522898851, + "median": 110.39003522898851, + "min": 92.0, + "q1": 102.39164829116073, + "q3": 118.38842216681628, + "std_dev": 7.998386937827774 + }, + "complexity": { + "max": 22.0, + "mean": 19.168092601912516, + "median": 19.0, + "min": 16.0, + "q1": 18.0, + "q3": 20.0, + "std_dev": 1.3011274331761224 + }, + "course_id": "ICS211", + "delay": { + "max": 5.0, + "mean": 4.490689481630587, + "median": 4.490689481630587, + "min": 4.0, + "q1": 4.0, + "q3": 4.9906027883625566, + "std_dev": 0.4999133067319703 + }, + "plan_count": 7948 + }, + "name": "Introduction to Computer Science II", + "number": "211", + "prefix": "ICS", + "prerequisites": "ICS111", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS212": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 7.0, + "mean": 5.359838953195775, + "median": 5.359838953195775, + "min": 5.0, + "q1": 5.0, + "q3": 5.9009124395143076, + "std_dev": 0.5410734863185326 + }, + "centrality": { + "max": 28.0, + "mean": 21.092098641167517, + "median": 21.092098641167517, + "min": 20.0, + "q1": 20.0, + "q3": 22.93058803615838, + "std_dev": 1.8384893949908632 + }, + "complexity": { + "max": 12.0, + "mean": 9.47559134373429, + "median": 9.0, + "min": 9.0, + "q1": 9.0, + "q3": 10.0, + "std_dev": 0.7619427200860007 + }, + "course_id": "ICS212", + "delay": { + "max": 5.0, + "mean": 4.1157523905385025, + "median": 4.1157523905385025, + "min": 4.0, + "q1": 4.0, + "q3": 4.435680155234102, + "std_dev": 0.3199277646955995 + }, + "plan_count": 7948 + }, + "name": "Program Structure", + "number": "212", + "prefix": "ICS", + "prerequisites": "ICS211", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS215": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "name": "Introduction to Scripting", + "number": "215", + "prefix": "ICS", + "prerequisites": "ICS111", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS222": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS222", + "delay": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "plan_count": 860 + }, + "name": "Basic Concepts of Computer Science", + "number": "222", + "prefix": "ICS", + "prerequisites": "ICS141", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS235": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "cross_listed_as": [ + "DATA235" + ], + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 3.0, + "mean": 3.0, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 3.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "course_id": "ICS235", + "delay": { + "max": 3.0, + "mean": 3.0, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 3.0, + "std_dev": 0.0 + }, + "plan_count": 976 + }, + "name": "Machine Learning Methods", + "number": "235", + "prefix": "ICS", + "prerequisites": { + "or": [ + "MATH241", + "MATH203", + "MATH215", + "MATH251A" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall" + ] + }, + "ICS241": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 17.0, + "mean": 13.856316054353284, + "median": 13.856316054353284, + "min": 10.0, + "q1": 12.245851059231606, + "q3": 15.466781049474962, + "std_dev": 1.6104649951216772 + }, + "centrality": { + "max": 138.0, + "mean": 85.98263714141939, + "median": 85.98263714141939, + "min": 45.0, + "q1": 55.25651527151433, + "q3": 116.70875901132445, + "std_dev": 30.72612186990506 + }, + "complexity": { + "max": 23.0, + "mean": 19.347005535983907, + "median": 20.0, + "min": 15.0, + "q1": 18.0, + "q3": 21.0, + "std_dev": 1.7909406427847938 + }, + "course_id": "ICS241", + "delay": { + "max": 6.0, + "mean": 5.490689481630587, + "median": 5.490689481630587, + "min": 5.0, + "q1": 5.0, + "q3": 5.9906027883625566, + "std_dev": 0.4999133067319703 + }, + "plan_count": 7948 + }, + "name": "Discrete Mathematics for Computer Science II", + "number": "241", + "prefix": "ICS", + "prerequisites": "ICS141", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS311": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 14.0, + "mean": 11.561147458480129, + "median": 11.561147458480129, + "min": 9.0, + "q1": 10.540642885146625, + "q3": 12.581652031813633, + "std_dev": 1.020504573333505 + }, + "centrality": { + "max": 242.0, + "mean": 201.97382989431227, + "median": 201.97382989431227, + "min": 162.0, + "q1": 185.03974353268288, + "q3": 218.90791625594167, + "std_dev": 16.934086361629387 + }, + "complexity": { + "max": 20.0, + "mean": 17.051836940110608, + "median": 17.0, + "min": 14.0, + "q1": 16.0, + "q3": 18.0, + "std_dev": 1.3114916948310291 + }, + "course_id": "ICS311", + "delay": { + "max": 6.0, + "mean": 5.490689481630587, + "median": 5.490689481630587, + "min": 5.0, + "q1": 5.0, + "q3": 5.9906027883625566, + "std_dev": 0.4999133067319703 + }, + "plan_count": 7948 + }, + "name": "Algorithms", + "number": "311", + "prefix": "ICS", + "prerequisites": { + "or": [ + { + "and": [ + "ICS211", + { + "or": [ + "ICS241", + "ECE362" + ] + }, + { + "or": [ + "MATH216", + "MATH242", + "MATH252A" + ] + } + ] + }, + { + "and": [ + "MATH301", + "MATH372" + ] + } + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS312": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS312", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 7948 + }, + "name": "Machine-Level and Systems Programming", + "number": "312", + "prefix": "ICS", + "prerequisites": { + "and": [ + { + "or": [ + "ICS212", + "ECE205" + ] + }, + { + "or": [ + "ICS311", + "ECE367" + ] + }, + "ICS314" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS313": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS313", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 7948 + }, + "name": "Programming Language Theory", + "number": "313", + "prefix": "ICS", + "prerequisites": { + "and": [ + { + "or": [ + "ICS212", + "ECE205" + ] + }, + { + "or": [ + "ICS311", + "ECE367" + ] + }, + "ICS314" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS314": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 14.0, + "mean": 11.323603422244611, + "median": 11.323603422244611, + "min": 9.0, + "q1": 10.271148530961105, + "q3": 12.376058313528118, + "std_dev": 1.0524548912835068 + }, + "centrality": { + "max": 188.0, + "mean": 148.27352793155478, + "median": 148.27352793155478, + "min": 117.0, + "q1": 134.35327483813722, + "q3": 162.19378102497234, + "std_dev": 13.920253093417555 + }, + "complexity": { + "max": 20.0, + "mean": 16.814292903875113, + "median": 17.0, + "min": 14.0, + "q1": 16.0, + "q3": 18.0, + "std_dev": 1.3572808902221476 + }, + "course_id": "ICS314", + "delay": { + "max": 6.0, + "mean": 5.490689481630587, + "median": 5.490689481630587, + "min": 5.0, + "q1": 5.0, + "q3": 5.9906027883625566, + "std_dev": 0.4999133067319703 + }, + "plan_count": 7948 + }, + "name": "Software Engineering I", + "number": "314", + "prefix": "ICS", + "prerequisites": { + "and": [ + { + "or": [ + "ICS211", + "ECE205" + ] + }, + { + "or": [ + "ICS241", + "ECE362", + { + "and": [ + "MATH301", + "MATH372" + ] + } + ] + } + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS321": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 2.0, + "mean": 0.2511323603422245, + "median": 0.2511323603422245, + "min": 0.0, + "q1": 0.0, + "q3": 0.7139876332063585, + "std_dev": 0.46285527286413397 + }, + "centrality": { + "max": 78.0, + "mean": 9.668595873175628, + "median": 9.668595873175628, + "min": 0.0, + "q1": 0.0, + "q3": 27.490468943352184, + "std_dev": 17.821873070176558 + }, + "complexity": { + "max": 8.0, + "mean": 5.489179667840973, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.8822231842245942 + }, + "course_id": "ICS321", + "delay": { + "max": 6.0, + "mean": 5.238047307498725, + "median": 5.238047307498725, + "min": 5.0, + "q1": 5.0, + "q3": 5.663935540361927, + "std_dev": 0.4258882328632024 + }, + "plan_count": 7948 + }, + "name": "Database Systems I", + "number": "321", + "prefix": "ICS", + "prerequisites": { + "and": [ + { + "or": [ + "ICS311", + "ECE367" + ] + }, + "ICS314" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS331": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 1.0, + "mean": 0.1157523905385002, + "median": 0.1157523905385002, + "min": 0.0, + "q1": 0.0, + "q3": 0.4356801552341001, + "std_dev": 0.3199277646955999 + }, + "centrality": { + "max": 44.0, + "mean": 5.035228988424754, + "median": 5.035228988424754, + "min": 0.0, + "q1": 0.0, + "q3": 18.953126391705126, + "std_dev": 13.917897403280373 + }, + "complexity": { + "max": 7.0, + "mean": 5.231504781077005, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.6398555293912 + }, + "course_id": "ICS331", + "delay": { + "max": 6.0, + "mean": 5.1157523905385025, + "median": 5.1157523905385025, + "min": 5.0, + "q1": 5.0, + "q3": 5.435680155234102, + "std_dev": 0.3199277646955995 + }, + "plan_count": 7948 + }, + "name": "Logic Design and Microprocessors", + "number": "331", + "prefix": "ICS", + "prerequisites": { + "and": [ + { + "or": [ + "ICS212", + "ECE205" + ] + }, + { + "or": [ + "ICS311", + "ECE367" + ] + }, + "ICS314" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS332": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 1.0, + "mean": 0.11273276295923498, + "median": 0.11273276295923498, + "min": 0.0, + "q1": 0.0, + "q3": 0.4289986117554845, + "std_dev": 0.3162658487962495 + }, + "centrality": { + "max": 39.0, + "mean": 4.3402113739305275, + "median": 4.3402113739305275, + "min": 0.0, + "q1": 0.0, + "q3": 16.517603800746294, + "std_dev": 12.177392426815766 + }, + "complexity": { + "max": 7.0, + "mean": 5.225465525918484, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.6325316975924992 + }, + "course_id": "ICS332", + "delay": { + "max": 6.0, + "mean": 5.112732762959234, + "median": 5.112732762959234, + "min": 5.0, + "q1": 5.0, + "q3": 5.428998611755484, + "std_dev": 0.3162658487962493 + }, + "plan_count": 7948 + }, + "name": "Operating Systems", + "number": "332", + "prefix": "ICS", + "prerequisites": { + "and": [ + { + "or": [ + "ICS311", + "ECE367" + ] + }, + "ICS314" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS351": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS351", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 7948 + }, + "name": "Network Design and Management", + "number": "351", + "prefix": "ICS", + "prerequisites": { + "and": [ + { + "or": [ + "ICS311", + "ECE367" + ] + }, + "ICS314" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall" + ] + }, + "ICS355": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 3.0, + "mean": 0.5903371917463537, + "median": 0.5903371917463537, + "min": 0.0, + "q1": 0.0, + "q3": 1.2870598648247045, + "std_dev": 0.6967226730783509 + }, + "centrality": { + "max": 30.0, + "mean": 5.608203321590303, + "median": 5.608203321590303, + "min": 0.0, + "q1": 0.0, + "q3": 12.242798955682147, + "std_dev": 6.634595634091845 + }, + "complexity": { + "max": 8.0, + "mean": 5.067941620533472, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 6.0, + "std_dev": 1.1626263331580708 + }, + "course_id": "ICS355", + "delay": { + "max": 5.0, + "mean": 4.477604428787111, + "median": 4.477604428787111, + "min": 4.0, + "q1": 4.0, + "q3": 4.977102615360444, + "std_dev": 0.4994981865733332 + }, + "plan_count": 7948 + }, + "name": "Security and Trust I - Resource Protections", + "number": "355", + "prefix": "ICS", + "prerequisites": { + "or": [ + "ICS241", + { + "and": [ + "MATH301", + "MATH372" + ] + } + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS361": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS361", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 7948 + }, + "name": "Introduction to Artificial Intelligence Programming", + "number": "361", + "prefix": "ICS", + "prerequisites": { + "and": [ + { + "or": [ + "ICS215", + "ICS212", + "ECE205" + ] + }, + { + "or": [ + "ICS311", + "ECE367" + ] + }, + "ICS314" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS369": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "cross_listed_as": [ + "ECE369" + ], + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 3.0, + "mean": 3.0, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 3.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 4.0, + "mean": 4.0, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.0 + }, + "course_id": "ICS369", + "delay": { + "max": 3.0, + "mean": 3.0, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 3.0, + "std_dev": 0.0 + }, + "plan_count": 964 + }, + "name": "Computational Media Systems", + "number": "369", + "prefix": "ICS", + "prerequisites": { + "or": [ + "ICS111", + "ECE160" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS414": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS414", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 1032 + }, + "name": "Software Engineering II", + "number": "414", + "prefix": "ICS", + "prerequisites": { + "and": [ + { + "or": [ + "ICS311", + "ECE367" + ] + }, + "ICS314" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS415": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS415", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 1028 + }, + "name": "Introduction to Programming for the Web", + "number": "415", + "prefix": "ICS", + "prerequisites": { + "and": [ + { + "or": [ + "ICS311", + "ECE367" + ] + }, + "ICS314" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS421": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 6.0, + "median": 6.0, + "min": 6.0, + "q1": 6.0, + "q3": 6.0, + "std_dev": 0.0 + }, + "course_id": "ICS421", + "delay": { + "max": 6.0, + "mean": 6.0, + "median": 6.0, + "min": 6.0, + "q1": 6.0, + "q3": 6.0, + "std_dev": 0.0 + }, + "plan_count": 972 + }, + "name": "Database Systems II", + "number": "421", + "prefix": "ICS", + "prerequisites": "ICS321", + "strict_corequisites": [], + "typically_offered": [ + "fall" + ] + }, + "ICS422": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "cross_listed_as": [ + "DATA422" + ], + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS422", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 912 + }, + "name": "Data Processing", + "number": "422", + "prefix": "ICS", + "prerequisites": { + "or": [ + "ICS311", + "ECE367" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "spring" + ] + }, + "ICS423": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS423", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 860 + }, + "name": "Data Security and Cryptography I", + "number": "423", + "prefix": "ICS", + "prerequisites": { + "and": [ + "ICS222", + "ICS355" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall" + ] + }, + "ICS424": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS424", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 968 + }, + "name": "Application Frameworks", + "number": "424", + "prefix": "ICS", + "prerequisites": { + "and": [ + { + "or": [ + "ICS311", + "ECE367" + ] + }, + "ICS314" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "spring" + ] + }, + "ICS425": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS425", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 1008 + }, + "name": "Computer Security and Ethics", + "number": "425", + "prefix": "ICS", + "prerequisites": "ICS355", + "strict_corequisites": [], + "typically_offered": [ + "fall" + ] + }, + "ICS426": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS426", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 908 + }, + "name": "Computer System Security", + "number": "426", + "prefix": "ICS", + "prerequisites": "ICS355", + "strict_corequisites": [], + "typically_offered": [ + "spring" + ] + }, + "ICS428": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS428", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 972 + }, + "name": "Digital Forensics", + "number": "428", + "prefix": "ICS", + "prerequisites": "ICS355", + "strict_corequisites": [], + "typically_offered": [ + "spring" + ] + }, + "ICS431": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 6.0, + "median": 6.0, + "min": 6.0, + "q1": 6.0, + "q3": 6.0, + "std_dev": 0.0 + }, + "course_id": "ICS431", + "delay": { + "max": 6.0, + "mean": 6.0, + "median": 6.0, + "min": 6.0, + "q1": 6.0, + "q3": 6.0, + "std_dev": 0.0 + }, + "plan_count": 920 + }, + "name": "Computer Architecture", + "number": "431", + "prefix": "ICS", + "prerequisites": { + "or": [ + "ICS331", + "ECE361" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "spring" + ] + }, + "ICS432": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 6.0, + "median": 6.0, + "min": 6.0, + "q1": 6.0, + "q3": 6.0, + "std_dev": 0.0 + }, + "course_id": "ICS432", + "delay": { + "max": 6.0, + "mean": 6.0, + "median": 6.0, + "min": 6.0, + "q1": 6.0, + "q3": 6.0, + "std_dev": 0.0 + }, + "plan_count": 896 + }, + "name": "Concurrent and High-Performance Programming", + "number": "432", + "prefix": "ICS", + "prerequisites": { + "and": [ + { + "or": [ + "ICS212", + "ECE205" + ] + }, + "ICS332", + { + "or": [ + "ICS311", + "ECE367" + ] + }, + "ICS314" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall" + ] + }, + "ICS434": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "cross_listed_as": [ + "DATA434" + ], + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 3.0, + "mean": 3.0, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 3.0, + "std_dev": 0.0 + }, + "course_id": "ICS434", + "delay": { + "max": 3.0, + "mean": 3.0, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 3.0, + "std_dev": 0.0 + }, + "plan_count": 924 + }, + "name": "Data Science Fundamentals", + "number": "434", + "prefix": "ICS", + "prerequisites": "ICS211", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS435": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "cross_listed_as": [ + "DATA435" + ], + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 3.0, + "mean": 3.0, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 3.0, + "std_dev": 0.0 + }, + "course_id": "ICS435", + "delay": { + "max": 3.0, + "mean": 3.0, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 3.0, + "std_dev": 0.0 + }, + "plan_count": 976 + }, + "name": "Machine Learning Fundamentals", + "number": "435", + "prefix": "ICS", + "prerequisites": "ICS235", + "strict_corequisites": [], + "typically_offered": [ + "fall" + ] + }, + "ICS438": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "cross_listed_as": [ + "DATA438" + ], + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 6.0, + "median": 6.0, + "min": 6.0, + "q1": 6.0, + "q3": 6.0, + "std_dev": 0.0 + }, + "course_id": "ICS438", + "delay": { + "max": 6.0, + "mean": 6.0, + "median": 6.0, + "min": 6.0, + "q1": 6.0, + "q3": 6.0, + "std_dev": 0.0 + }, + "plan_count": 1024 + }, + "name": "Big Data Analytics", + "number": "438", + "prefix": "ICS", + "prerequisites": "ICS321", + "strict_corequisites": [], + "typically_offered": [ + "spring" + ] + }, + "ICS441": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS441", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 864 + }, + "name": "Theory of Computation", + "number": "441", + "prefix": "ICS", + "prerequisites": { + "and": [ + { + "or": [ + "ICS311", + "ECE367" + ] + }, + "ICS314" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall" + ] + }, + "ICS442": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS442", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 1044 + }, + "name": "Analytical Models and Methods", + "number": "442", + "prefix": "ICS", + "prerequisites": { + "and": [ + { + "or": [ + "ICS311", + "ECE367" + ] + }, + "ICS314" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "spring" + ] + }, + "ICS443": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS443", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 1008 + }, + "name": "Parallel Algorithms", + "number": "443", + "prefix": "ICS", + "prerequisites": { + "and": [ + { + "or": [ + "ICS311", + "ECE367" + ] + }, + "ICS314" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall" + ] + }, + "ICS451": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS451", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 7948 + }, + "name": "Data Networks", + "number": "451", + "prefix": "ICS", + "prerequisites": { + "and": [ + { + "or": [ + "ICS212", + "ECE205" + ] + }, + { + "or": [ + "ICS311", + "ECE367" + ] + }, + "ICS314" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS452": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "ICS452", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 1736 + }, + "name": "Software Design for Robotics", + "number": "452", + "prefix": "ICS", + "strict_corequisites": [], + "typically_offered": [ + "spring" + ] + }, + "ICS455": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS455", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 944 + }, + "name": "Security and Trust II - Information Assurance", + "number": "455", + "prefix": "ICS", + "prerequisites": "ICS355", + "strict_corequisites": [], + "typically_offered": [ + "spring" + ] + }, + "ICS461": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS461", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 976 + }, + "name": "Artificial Intelligence", + "number": "461", + "prefix": "ICS", + "prerequisites": { + "or": [ + "ICS311", + "ECE367" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS462": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS462", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 1044 + }, + "name": "Artificial Intelligence for Games", + "number": "462", + "prefix": "ICS", + "prerequisites": { + "and": [ + { + "or": [ + "ICS212", + "ECE205" + ] + }, + { + "or": [ + "ICS311", + "ECE367" + ] + }, + "ICS314", + { + "or": [ + "PHYS151", + "PHYS170" + ] + } + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "spring" + ] + }, + "ICS464": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS464", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 924 + }, + "name": "Human Computer Interaction I", + "number": "464", + "prefix": "ICS", + "prerequisites": { + "and": [ + { + "or": [ + "ICS311", + "ECE367" + ] + }, + "ICS314" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS466": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS466", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 952 + }, + "name": "Design for Mobile Devices", + "number": "466", + "prefix": "ICS", + "prerequisites": { + "and": [ + { + "or": [ + "ICS311", + "ECE367" + ] + }, + "ICS314" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "spring" + ] + }, + "ICS469": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS469", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 1076 + }, + "name": "Cognitive Science", + "number": "469", + "prefix": "ICS", + "prerequisites": { + "and": [ + { + "or": [ + "ICS311", + "ECE367" + ] + }, + "ICS314" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall" + ] + }, + "ICS481": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 1.0, + "mean": 0.5348314606741579, + "median": 0.5348314606741579, + "min": 0.0, + "q1": 0.036046166837917726, + "q3": 1.0, + "std_dev": 0.49878529383624015 + }, + "centrality": { + "max": 39.0, + "mean": 20.591011235955055, + "median": 20.591011235955055, + "min": 0.0, + "q1": 1.3842963494834422, + "q3": 39.0, + "std_dev": 19.206714886471612 + }, + "complexity": { + "max": 7.0, + "mean": 6.06966292134831, + "median": 7.0, + "min": 5.0, + "q1": 5.0, + "q3": 7.0, + "std_dev": 0.9975705876724803 + }, + "course_id": "ICS481", + "delay": { + "max": 6.0, + "mean": 5.534831460674153, + "median": 5.534831460674153, + "min": 5.0, + "q1": 5.036046166837912, + "q3": 6.0, + "std_dev": 0.49878529383624015 + }, + "plan_count": 1780 + }, + "name": "Introduction to Computer Graphics", + "number": "481", + "prefix": "ICS", + "prerequisites": { + "and": [ + { + "or": [ + "ICS311", + "ECE367" + ] + }, + "ICS314" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall" + ] + }, + "ICS484": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "cross_listed_as": [ + "DATA484", + "CINE484" + ], + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS484", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 992 + }, + "name": "Data Visualization", + "number": "484", + "prefix": "ICS", + "prerequisites": { + "and": [ + { + "or": [ + "ICS311", + "ECE367" + ] + }, + "ICS314" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "spring" + ] + }, + "ICS485": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS485", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 992 + }, + "name": "Video Game Design and Development", + "number": "485", + "prefix": "ICS", + "prerequisites": { + "and": [ + { + "or": [ + "ICS311", + "ECE367" + ] + }, + "ICS314" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall" + ] + }, + "ICS487": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 3.0, + "mean": 3.0, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 3.0, + "std_dev": 0.0 + }, + "course_id": "ICS487", + "delay": { + "max": 3.0, + "mean": 3.0, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 3.0, + "std_dev": 0.0 + }, + "plan_count": 964 + }, + "name": "Digital Content Creation I (VR/AR)", + "number": "487", + "prefix": "ICS", + "prerequisites": "ICS369", + "strict_corequisites": [], + "typically_offered": [ + "fall" + ] + }, + "ICS488": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 6.0, + "mean": 6.0, + "median": 6.0, + "min": 6.0, + "q1": 6.0, + "q3": 6.0, + "std_dev": 0.0 + }, + "course_id": "ICS488", + "delay": { + "max": 6.0, + "mean": 6.0, + "median": 6.0, + "min": 6.0, + "q1": 6.0, + "q3": 6.0, + "std_dev": 0.0 + }, + "plan_count": 952 + }, + "name": "Digital Content Creation II (Advanced VFX)", + "number": "488", + "prefix": "ICS", + "prerequisites": "ICS481", + "strict_corequisites": [], + "typically_offered": [ + "spring" + ] + }, + "ICS491": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "max_repeat_credits": 6.0, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "ICS491", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 1068 + }, + "name": "Special Topics", + "number": "491", + "prefix": "ICS", + "repeatable": true, + "strict_corequisites": [] + }, + "ICS495": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "max_repeat_credits": 9.0, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "ICS495", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 932 + }, + "name": "Special Topics in Security", + "number": "495", + "prefix": "ICS", + "repeatable": true, + "strict_corequisites": [] + }, + "ICS496": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "course_id": "ICS496", + "delay": { + "max": 5.0, + "mean": 5.0, + "median": 5.0, + "min": 5.0, + "q1": 5.0, + "q3": 5.0, + "std_dev": 0.0 + }, + "plan_count": 7948 + }, + "name": "Capstone Project", + "number": "496", + "prefix": "ICS", + "prerequisites": { + "and": [ + { + "or": [ + "ICS311", + "ECE367" + ] + }, + "ICS314" + ] + }, + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "ICS499": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 0.0, + "credit_range": { + "max": 3, + "min": 1 + }, + "csv_id": null, + "id": null, + "max_repeat_credits": 6.0, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "ICS499", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 1012 + }, + "name": "Directed Reading/Research", + "number": "499", + "prefix": "ICS", + "repeatable": true, + "strict_corequisites": [] + }, + "MATH203": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "gen_ed_attributes": [ + "FQ" + ], + "id": null, + "name": "Calculus for Business and Social Sciences", + "number": "203", + "prefix": "MATH", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "MATH215": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "gen_ed_attributes": [ + "FQ" + ], + "id": null, + "metrics": { + "blocking": { + "max": 27.0, + "mean": 22.505284348263714, + "median": 22.505284348263714, + "min": 18.0, + "q1": 21.27322752192687, + "q3": 23.73734117460056, + "std_dev": 1.2320568263368439 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 33.0, + "mean": 27.995973829894318, + "median": 28.0, + "min": 23.0, + "q1": 27.0, + "q3": 29.0, + "std_dev": 1.4091275878900287 + }, + "course_id": "MATH215", + "delay": { + "max": 6.0, + "mean": 5.490689481630587, + "median": 5.490689481630587, + "min": 5.0, + "q1": 5.0, + "q3": 5.9906027883625566, + "std_dev": 0.4999133067319703 + }, + "plan_count": 7948 + }, + "name": "Applied Calculus I", + "number": "215", + "prefix": "MATH", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "MATH216": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 19.0, + "mean": 16.65148465022647, + "median": 16.65148465022647, + "min": 13.0, + "q1": 15.631267990218541, + "q3": 17.6717013102344, + "std_dev": 1.0202166600079303 + }, + "centrality": { + "max": 195.0, + "mean": 139.90286864620123, + "median": 139.90286864620123, + "min": 94.0, + "q1": 115.05264034083658, + "q3": 164.75309695156588, + "std_dev": 24.850228305364656 + }, + "complexity": { + "max": 25.0, + "mean": 22.14217413185707, + "median": 22.0, + "min": 18.0, + "q1": 21.0, + "q3": 23.0, + "std_dev": 1.260953180286196 + }, + "course_id": "MATH216", + "delay": { + "max": 6.0, + "mean": 5.490689481630587, + "median": 5.490689481630587, + "min": 5.0, + "q1": 5.0, + "q3": 5.9906027883625566, + "std_dev": 0.4999133067319703 + }, + "plan_count": 7948 + }, + "name": "Applied Calculus II", + "number": "216", + "prefix": "MATH", + "prerequisites": "MATH215", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "MATH241": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "gen_ed_attributes": [ + "FQ" + ], + "id": null, + "name": "Calculus I", + "number": "241", + "prefix": "MATH", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "MATH242": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Calculus II", + "number": "242", + "prefix": "MATH", + "prerequisites": "MATH241", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "MATH251A": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "gen_ed_attributes": [ + "FQ" + ], + "id": null, + "name": "Accelerated Calculus I", + "number": "251A", + "prefix": "MATH", + "strict_corequisites": [], + "typically_offered": [ + "fall" + ] + }, + "MATH252A": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 4.0, + "csv_id": null, + "id": null, + "name": "Accelerated Calculus II", + "number": "252A", + "prefix": "MATH", + "prerequisites": "MATH251A", + "strict_corequisites": [], + "typically_offered": [ + "spring" + ] + }, + "MATH301": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 17.0, + "mean": 15.151484650226466, + "median": 15.151484650226466, + "min": 12.0, + "q1": 14.262191653876142, + "q3": 16.040777646576792, + "std_dev": 0.8892929963503247 + }, + "centrality": { + "max": 111.0, + "mean": 92.87770508303976, + "median": 92.87770508303976, + "min": 75.0, + "q1": 86.06349502986363, + "q3": 99.69191513621588, + "std_dev": 6.814210053176121 + }, + "complexity": { + "max": 22.0, + "mean": 19.642174131857146, + "median": 20.0, + "min": 16.0, + "q1": 19.0, + "q3": 20.0, + "std_dev": 1.1575849527675588 + }, + "course_id": "MATH301", + "delay": { + "max": 5.0, + "mean": 4.490689481630592, + "median": 4.490689481630592, + "min": 4.0, + "q1": 4.0, + "q3": 4.990602788362562, + "std_dev": 0.49991330673197026 + }, + "plan_count": 3974 + }, + "name": "Introduction to Discrete Mathematics", + "number": "301", + "prefix": "MATH", + "prerequisites": "MATH241", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "MATH307": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 3.0, + "mean": 3.0, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 3.0, + "std_dev": 0.0 + }, + "course_id": "MATH307", + "delay": { + "max": 3.0, + "mean": 3.0, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 3.0, + "std_dev": 0.0 + }, + "plan_count": 3974 + }, + "name": "Linear Algebra and Differential Equations", + "number": "307", + "prefix": "MATH", + "prerequisites": "MATH242", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "MATH372": { + "canonical_name": null, + "corequisites": [], + "credit_hours": 3.0, + "csv_id": null, + "id": null, + "metrics": { + "blocking": { + "max": 17.0, + "mean": 15.151484650226479, + "median": 15.151484650226479, + "min": 12.0, + "q1": 14.262191653876153, + "q3": 16.040777646576803, + "std_dev": 0.8892929963503252 + }, + "centrality": { + "max": 138.0, + "mean": 115.9280322093608, + "median": 115.9280322093608, + "min": 94.0, + "q1": 107.40355640298436, + "q3": 124.45250801573722, + "std_dev": 8.52447580637643 + }, + "complexity": { + "max": 23.0, + "mean": 20.64217413185702, + "median": 21.0, + "min": 17.0, + "q1": 20.0, + "q3": 21.0, + "std_dev": 1.157584952767557 + }, + "course_id": "MATH372", + "delay": { + "max": 6.0, + "mean": 5.490689481630587, + "median": 5.490689481630587, + "min": 5.0, + "q1": 5.0, + "q3": 5.9906027883625566, + "std_dev": 0.4999133067319703 + }, + "plan_count": 7948 + }, + "name": "Elementary Probability and Statistics", + "number": "372", + "prefix": "MATH", + "prerequisites": "MATH242", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "PHYS151": { + "canonical_name": null, + "corequisites": [ + "PHYS151L" + ], + "credit_hours": 3.0, + "csv_id": null, + "gen_ed_attributes": [ + "DP" + ], + "id": null, + "name": "College Physics I", + "number": "151", + "prefix": "PHYS", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "PHYS151L": { + "canonical_name": null, + "corequisites": [ + "PHYS151" + ], + "credit_hours": 1.0, + "csv_id": null, + "gen_ed_attributes": [ + "DY" + ], + "id": null, + "name": "College Physics I Lab", + "number": "151L", + "prefix": "PHYS", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "PHYS152": { + "canonical_name": null, + "corequisites": [ + "PHYS152L" + ], + "credit_hours": 3.0, + "csv_id": null, + "gen_ed_attributes": [ + "DP" + ], + "id": null, + "name": "College Physics II", + "number": "152", + "prefix": "PHYS", + "prerequisites": "PHYS151", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "PHYS152L": { + "canonical_name": null, + "corequisites": [ + "PHYS152" + ], + "credit_hours": 1.0, + "csv_id": null, + "gen_ed_attributes": [ + "DY" + ], + "id": null, + "name": "College Physics II Lab", + "number": "152L", + "prefix": "PHYS", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "PHYS170": { + "canonical_name": null, + "corequisites": [ + "PHYS170L" + ], + "credit_hours": 4.0, + "csv_id": null, + "gen_ed_attributes": [ + "DP" + ], + "id": null, + "metrics": { + "blocking": { + "max": 2.0, + "mean": 1.1313537996980374, + "median": 1.1313537996980374, + "min": 1.0, + "q1": 1.0, + "q3": 1.4691406836987713, + "std_dev": 0.337786884000734 + }, + "centrality": { + "max": 6.0, + "mean": 3.394061399094123, + "median": 3.394061399094123, + "min": 3.0, + "q1": 3.0, + "q3": 4.407422051096326, + "std_dev": 1.0133606520022034 + }, + "complexity": { + "max": 5.0, + "mean": 4.1313537996980285, + "median": 4.0, + "min": 4.0, + "q1": 4.0, + "q3": 4.0, + "std_dev": 0.3377868840007337 + }, + "course_id": "PHYS170", + "delay": { + "max": 3.0, + "mean": 3.0, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 3.0, + "std_dev": 0.0 + }, + "plan_count": 7948 + }, + "name": "General Physics I (Calculus)", + "number": "170", + "prefix": "PHYS", + "prerequisites": "MATH241", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "PHYS170L": { + "canonical_name": null, + "corequisites": [ + "PHYS170" + ], + "credit_hours": 1.0, + "csv_id": null, + "gen_ed_attributes": [ + "DY" + ], + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "PHYS170L", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 7948 + }, + "name": "General Physics I Lab", + "number": "170L", + "prefix": "PHYS", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "PHYS272": { + "canonical_name": null, + "corequisites": [ + "PHYS272L" + ], + "credit_hours": 3.0, + "csv_id": null, + "gen_ed_attributes": [ + "DP" + ], + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 3.0, + "mean": 3.0, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 3.0, + "std_dev": 0.0 + }, + "course_id": "PHYS272", + "delay": { + "max": 3.0, + "mean": 3.0, + "median": 3.0, + "min": 3.0, + "q1": 3.0, + "q3": 3.0, + "std_dev": 0.0 + }, + "plan_count": 7948 + }, + "name": "General Physics II (Calculus)", + "number": "272", + "prefix": "PHYS", + "prerequisites": "PHYS170", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + }, + "PHYS272L": { + "canonical_name": null, + "corequisites": [ + "PHYS272" + ], + "credit_hours": 1.0, + "csv_id": null, + "gen_ed_attributes": [ + "DY" + ], + "id": null, + "metrics": { + "blocking": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "centrality": { + "max": 0.0, + "mean": 0.0, + "median": 0.0, + "min": 0.0, + "q1": 0.0, + "q3": 0.0, + "std_dev": 0.0 + }, + "complexity": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "course_id": "PHYS272L", + "delay": { + "max": 1.0, + "mean": 1.0, + "median": 1.0, + "min": 1.0, + "q1": 1.0, + "q3": 1.0, + "std_dev": 0.0 + }, + "plan_count": 7948 + }, + "name": "General Physics II Lab", + "number": "272L", + "prefix": "PHYS", + "strict_corequisites": [], + "typically_offered": [ + "fall", + "spring" + ] + } + } +} diff --git a/src/cli/args.rs b/src/cli/args.rs index 164f394..31e9f40 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -199,10 +199,21 @@ pub enum DegreeSubcommand { /// Run full degree analysis: generate plans, compute metrics, produce /// HTML reports with statistics, and export CSV plan files. Analyze { - /// Degree YAML file(s) to analyze. - #[arg(value_name = "FILES", num_args = 1..)] + /// Degree YAML file(s) to analyze. Optional when `--from-db` is given; + /// otherwise at least one file is required. + #[arg(value_name = "FILES", num_args = 0..)] files: Vec, + /// Analyze a stored program's canonical degree fetched from the + /// database instead of a local file. The value matches a program by + /// exact `program_key`/`degree_id`, then falls back to a name + /// substring. Ambiguous matches are listed and the run stops. + /// Mutually exclusive with positional FILES; single-program only + /// (no worker pool / `--jobs`). + #[cfg(feature = "database")] + #[arg(long, value_name = "NAME")] + from_db: Option, + /// Calculation strategy for aggregate metrics (median or mean) #[arg(long, value_enum, value_name = "STRATEGY")] calc_strategy: Option, @@ -604,6 +615,79 @@ pub enum DbSubcommand { #[arg(long, default_value = "2023")] year: u16, }, + /// Import degree analysis report(s) into the normalized program tables. + /// + /// Each input is a degree-first analysis report (`*_report.json`) or a plain + /// unified degree (JSON/YAML). The shared import core parses the document, + /// resolves its institution against IPEDS, and upserts the program projection + /// (`programs`, `courses`, `program_courses`, `program_requirements`) plus, when + /// the report carries an `analysis` block, one analysis run with its course + /// metrics and selected plans. + /// + /// Each positional argument may be a file or a directory; a directory is + /// expanded to its `*_report.json` files (falling back to `*.json` when none + /// match). Overwriting an existing program requires `--replace` (unverified) or + /// `--force` (verified) — that explicit flag is the double-check, so there is no + /// interactive prompt. `--dry-run` reports the row counts without writing. + /// + /// Examples: + /// ```sh + /// nuanalytics db import metrics/neu-khoury-bscs-boston_report.json + /// nuanalytics db import metrics/ --dry-run + /// nuanalytics db import report.json --unitid 167358 --replace + /// ``` + Import { + /// Degree report / unified degree file(s) and/or directories. A directory + /// is expanded to its `*_report.json` files (fallback `*.json`). + #[arg(value_name = "FILES", num_args = 1..)] + files: Vec, + + /// Analysis-run variant label. `full` (default) writes the program + /// projection; a non-`full` variant only attaches an analysis run. + #[arg(long, value_name = "NAME")] + variant: Option, + + /// Override the resolved institution IPEDS unit id. + #[arg(long, value_name = "N")] + unitid: Option, + + /// Override the institution name used for resolution. + #[arg(long, value_name = "NAME")] + institution: Option, + + /// Override the CIP code (part of the natural program key). + #[arg(long, value_name = "CIP")] + cip: Option, + + /// Override the catalog year (part of the program identity). + #[arg(long, value_name = "YEAR")] + catalog: Option, + + /// Override the degree id. + #[arg(long = "degree-id", value_name = "ID")] + degree_id: Option, + + /// Overwrite a verified program / skip confirmation. + #[arg(long)] + force: bool, + + /// Replace an existing (unverified) program. + #[arg(long)] + replace: bool, + + /// Skip the program entirely if it already exists. + #[arg(long = "skip-existing")] + skip_existing: bool, + + /// Build the plan and report counts, but write nothing. + #[arg(long = "dry-run")] + dry_run: bool, + + /// Number of files to process concurrently. v1 always runs sequentially; + /// reserved for a future worker pool. + #[arg(short = 'j', long, value_name = "N", default_value_t = 1)] + jobs: usize, + }, } #[derive(Parser, Debug)] diff --git a/src/cli/commands/db.rs b/src/cli/commands/db.rs index 252a601..e545773 100644 --- a/src/cli/commands/db.rs +++ b/src/cli/commands/db.rs @@ -4,10 +4,12 @@ //! - `login` / `logout` / `whoami` — Supabase OAuth authentication //! - `status` — connectivity check //! - `ipeds-import` — IPEDS CSV ingestion +//! - `import` — degree report → normalized program tables use tokio::io::{AsyncReadExt, AsyncWriteExt}; use nu_analytics::config::Config; +use nu_analytics::database::import::{execute_import, ImportOptions, ImportOutcome, ImportResult}; use nu_analytics::database::{ auth_file_path, clear_auth_state, ipeds, load_auth_state, save_auth_state, AuthState, DbClient, }; @@ -31,6 +33,34 @@ pub fn run(subcommand: DbSubcommand, config: &Config) { completions, year, } => run_ipeds_import(config, dir.as_deref(), institutions, completions, year), + DbSubcommand::Import { + files, + variant, + unitid, + institution, + cip, + catalog, + degree_id, + force, + replace, + skip_existing, + dry_run, + jobs, + } => { + let opts = ImportOptions { + variant: variant.unwrap_or_else(|| "full".to_string()), + unitid, + institution, + cip_code: cip, + catalog_year: catalog, + degree_id, + force, + replace, + skip_existing, + dry_run, + }; + run_import(config, &files, &opts, jobs); + } } } @@ -39,7 +69,7 @@ pub fn run(subcommand: DbSubcommand, config: &Config) { // ============================================================================ /// Build a single-threaded Tokio runtime, printing an error and returning `None` on failure. -fn make_runtime() -> Option { +pub(super) fn make_runtime() -> Option { match tokio::runtime::Runtime::new() { Ok(rt) => Some(rt), Err(e) => { @@ -602,6 +632,362 @@ fn run_ipeds_import( } } +// ============================================================================ +// Degree import +// ============================================================================ + +/// Run `db import`: load degree report(s) into the normalized program tables. +/// +/// Expands any directory arguments to their report files, connects to the +/// database (printing login guidance if unauthenticated), then imports each +/// file. A single file prints a detailed outcome and exits non-zero on a +/// blocked result; a batch isolates per-file failures to `import_failures.log` +/// and prints a final summary. +fn run_import(config: &Config, files: &[std::path::PathBuf], opts: &ImportOptions, jobs: usize) { + // `--jobs` is accepted for forward-compatibility; v1 always runs + // sequentially (no worker pool yet — the heavy lifting is one network + // round-trip per file, so a pool buys little for the common batch sizes). + let _ = jobs; + + let inputs = collect_import_inputs(files); + if inputs.is_empty() { + eprintln!("✗ No degree report files to import."); + eprintln!(" Pass one or more *.json report files, or a directory containing them."); + std::process::exit(1); + } + + let Some(rt) = make_runtime() else { return }; + + let client = match rt.block_on(DbClient::from_config(&config.database)) { + Ok(c) => c, + Err(e) => { + eprintln!("✗ Database not available: {e}"); + eprintln!(" Configure the database and run `nuanalytics db login` first."); + std::process::exit(1); + } + }; + + let dry = if opts.dry_run { " (dry-run)" } else { "" }; + + if inputs.len() == 1 { + run_import_single(&rt, &client, &inputs[0], opts, dry); + } else { + run_import_batch(&rt, &client, &inputs, opts, dry); + } +} + +/// Import a single file with full diagnostics; exit non-zero when the import is +/// blocked (rejected / ambiguous / needs-confirmation) or a transport error +/// occurs. +fn run_import_single( + rt: &tokio::runtime::Runtime, + client: &DbClient, + path: &std::path::Path, + opts: &ImportOptions, + dry: &str, +) { + let text = match std::fs::read_to_string(path) { + Ok(t) => t, + Err(e) => { + eprintln!("✗ Cannot read {}: {e}", path.display()); + std::process::exit(1); + } + }; + + match rt.block_on(execute_import(client, &text, opts)) { + Ok(outcome) => { + print_import_outcome(path, &outcome, dry); + if is_blocked(&outcome.result) { + std::process::exit(1); + } + } + Err(e) => { + eprintln!("✗ Import failed for {}: {e}", path.display()); + std::process::exit(1); + } + } +} + +/// Running tally of batch outcomes by class. +#[derive(Default)] +struct ImportTally { + created: usize, + updated: usize, + skipped: usize, + needs_confirmation: usize, + ambiguous: usize, + rejected: usize, + errors: usize, +} + +/// Import many files with per-file isolation: a read/parse/transport error on +/// one file is logged and the run continues. Failures are written to +/// `import_failures.log`; a one-line progress is printed per file plus a final +/// summary. +fn run_import_batch( + rt: &tokio::runtime::Runtime, + client: &DbClient, + inputs: &[std::path::PathBuf], + opts: &ImportOptions, + dry: &str, +) { + let total = inputs.len(); + println!("Importing {total} report(s){dry}…"); + + let mut tally = ImportTally::default(); + let mut failures: Vec<(std::path::PathBuf, String)> = Vec::new(); + + for (idx, path) in inputs.iter().enumerate() { + let label = format!("[{}/{total}] {}", idx + 1, path.display()); + let text = match std::fs::read_to_string(path) { + Ok(t) => t, + Err(e) => { + tally.errors += 1; + failures.push((path.clone(), format!("read error: {e}"))); + println!(" ✗ {label}: read error: {e}"); + continue; + } + }; + match rt.block_on(execute_import(client, &text, opts)) { + Ok(outcome) => { + tally_outcome(&mut tally, &outcome.result); + println!( + " {} {label}: {}", + outcome_marker(&outcome.result), + result_label(&outcome.result) + ); + } + Err(e) => { + tally.errors += 1; + failures.push((path.clone(), e.to_string())); + println!(" ✗ {label}: {e}"); + } + } + } + + print_import_summary(&tally, total, dry); + write_import_failures(&failures); +} + +/// Expand `files` into a flat list of report files. A directory is expanded to +/// its `*_report.json` files, falling back to `*.json` when none match. A plain +/// file path is taken verbatim. Missing paths are warned about and skipped. +fn collect_import_inputs(files: &[std::path::PathBuf]) -> Vec { + let mut out = Vec::new(); + for path in files { + if path.is_dir() { + let expanded = collect_dir_reports(path); + if expanded.is_empty() { + eprintln!("⚠ No *.json report files found in {}", path.display()); + } + out.extend(expanded); + } else if path.exists() { + out.push(path.clone()); + } else { + eprintln!("⚠ Skipping missing path: {}", path.display()); + } + } + out +} + +/// List the report files directly under `dir`: `*_report.json` if any exist, +/// otherwise every `*.json`. Results are sorted for a deterministic order. +fn collect_dir_reports(dir: &std::path::Path) -> Vec { + let Ok(entries) = std::fs::read_dir(dir) else { + return Vec::new(); + }; + let json: Vec = entries + .flatten() + .map(|e| e.path()) + .filter(|p| { + p.is_file() + && p.extension() + .and_then(|e| e.to_str()) + .is_some_and(|e| e.eq_ignore_ascii_case("json")) + }) + .collect(); + let mut reports: Vec = json + .iter() + .filter(|p| { + p.file_name() + .and_then(|n| n.to_str()) + .is_some_and(|n| n.ends_with("_report.json")) + }) + .cloned() + .collect(); + if reports.is_empty() { + reports = json; + } + reports.sort(); + reports +} + +/// Whether a result requires the user to act before the import can proceed. +const fn is_blocked(result: &ImportResult) -> bool { + matches!( + result, + ImportResult::NeedsConfirmation(_) + | ImportResult::InstitutionAmbiguous(_) + | ImportResult::Rejected(_) + ) +} + +/// The `✓`/`⚠`/`✗` status marker for a result class. +const fn outcome_marker(result: &ImportResult) -> &'static str { + match result { + ImportResult::Created | ImportResult::Updated => "✓", + ImportResult::Skipped => "ℹ", + ImportResult::NeedsConfirmation(_) | ImportResult::InstitutionAmbiguous(_) => "⚠", + ImportResult::Rejected(_) => "✗", + } +} + +/// A short human label for a result class. +const fn result_label(result: &ImportResult) -> &'static str { + match result { + ImportResult::Created => "created", + ImportResult::Updated => "updated", + ImportResult::Skipped => "skipped", + ImportResult::NeedsConfirmation(_) => "needs confirmation", + ImportResult::InstitutionAmbiguous(_) => "institution ambiguous", + ImportResult::Rejected(_) => "rejected", + } +} + +/// Increment the batch tally for one result. +const fn tally_outcome(tally: &mut ImportTally, result: &ImportResult) { + match result { + ImportResult::Created => tally.created += 1, + ImportResult::Updated => tally.updated += 1, + ImportResult::Skipped => tally.skipped += 1, + ImportResult::NeedsConfirmation(_) => tally.needs_confirmation += 1, + ImportResult::InstitutionAmbiguous(_) => tally.ambiguous += 1, + ImportResult::Rejected(_) => tally.rejected += 1, + } +} + +/// Print the detailed outcome of a single import (result, resolved unit id, +/// row counts, conversion warnings, messages, and guidance when blocked). +fn print_import_outcome(path: &std::path::Path, outcome: &ImportOutcome, dry: &str) { + println!( + "{} {}: {}{dry}", + outcome_marker(&outcome.result), + path.display(), + result_label(&outcome.result) + ); + let institution = match (&outcome.institution, outcome.resolved_unitid) { + (Some(name), Some(u)) => format!("{name} (unitid {u})"), + (Some(name), None) => format!("{name} (unresolved)"), + (None, Some(u)) => format!("(unitid {u})"), + (None, None) => "(unresolved)".to_string(), + }; + println!(" institution: {institution}"); + println!(" program_key: {}", outcome.program_key); + println!(" variant: {}", outcome.variant); + // On the ambiguous path nothing is built (resolution stops early), so the + // analysis/row counts would be meaningless — skip them; the candidate list + // printed below is the actionable part. + if !matches!(outcome.result, ImportResult::InstitutionAmbiguous(_)) { + if outcome.run_written { + match (outcome.variations_run, outcome.sample_type.as_deref()) { + (Some(v), Some(s)) => println!( + " analysis: {v} variations ({s}), {} sample plans", + outcome.plans_written + ), + (Some(v), None) => { + println!( + " analysis: {v} variations, {} sample plans", + outcome.plans_written + ); + } + _ => println!(" analysis: {} sample plans", outcome.plans_written), + } + } else { + println!(" analysis: none (no metrics in upload)"); + } + println!( + " rows: {} courses, {} requirements, {} course-metrics", + outcome.courses_written, outcome.requirements_written, outcome.course_metrics_written + ); + } + + if !outcome.conversion_warnings.is_empty() { + println!(" conversion warnings:"); + for w in &outcome.conversion_warnings { + println!(" • {w}"); + } + } + if !outcome.messages.is_empty() { + println!(" messages:"); + for m in &outcome.messages { + println!(" • {m}"); + } + } + + print_blocked_guidance(&outcome.result); +} + +/// Print actionable guidance for a blocked result (rejected / ambiguous / +/// needs-confirmation); no-op for create/update/skip. +fn print_blocked_guidance(result: &ImportResult) { + match result { + ImportResult::Rejected(errors) => { + eprintln!(" ✗ report rejected:"); + for e in errors { + eprintln!(" • {e}"); + } + } + ImportResult::InstitutionAmbiguous(candidates) => { + eprintln!( + " ⚠ institution name matched multiple institutions — re-run with --unitid :" + ); + for (unitid, name) in candidates { + eprintln!(" • {unitid} {name}"); + } + } + ImportResult::NeedsConfirmation(reason) => { + eprintln!(" ⚠ {reason}"); + eprintln!(" re-run with --replace (unverified) or --force (verified) to overwrite"); + } + ImportResult::Created | ImportResult::Updated | ImportResult::Skipped => {} + } +} + +/// Print the final batch summary line(s). +fn print_import_summary(tally: &ImportTally, total: usize, dry: &str) { + println!( + "✓ imported {created} created, {updated} updated, {skipped} skipped of {total}{dry}", + created = tally.created, + updated = tally.updated, + skipped = tally.skipped, + ); + let attention = tally.needs_confirmation + tally.ambiguous + tally.rejected + tally.errors; + if attention > 0 { + println!( + " ⚠ {} needs-confirmation, {} ambiguous, {} rejected, {} error(s)", + tally.needs_confirmation, tally.ambiguous, tally.rejected, tally.errors + ); + } +} + +/// Write batch failures to `import_failures.log` in the current directory +/// (one `pathreason` line each). No-op when there are no failures. +fn write_import_failures(failures: &[(std::path::PathBuf, String)]) { + use std::fmt::Write as _; + + if failures.is_empty() { + return; + } + let mut body = String::new(); + for (path, reason) in failures { + let _ = writeln!(body, "{}\t{reason}", path.display()); + } + let log = std::path::Path::new("import_failures.log"); + if std::fs::write(log, body).is_ok() { + println!(" failures listed in {}", log.display()); + } +} + // ============================================================================ // Helpers // ============================================================================ diff --git a/src/cli/commands/degree.rs b/src/cli/commands/degree.rs index d0fbf37..67e523b 100644 --- a/src/cli/commands/degree.rs +++ b/src/cli/commands/degree.rs @@ -528,6 +528,215 @@ pub fn run_analyze(files: &[PathBuf], options: &AnalyzeOptions, config: &Config) } } +/// One stored `programs` row, projected to the columns needed to resolve and +/// analyze a program by name (the lossless `document` plus identity fields used +/// for the candidate list and the "loaded" banner). +#[cfg(feature = "database")] +#[derive(Debug, Clone, serde::Deserialize)] +struct StoredProgramRow { + program_key: String, + name: String, + unitid: Option, + institution_raw: Option, + catalog_year: Option, + document: serde_json::Value, +} + +/// Columns selected when resolving a `--from-db` program (lossless `document` +/// last, identity fields for the banner / candidate list first). +#[cfg(feature = "database")] +const FROM_DB_COLS: &str = + "program_key,degree_id,name,unitid,institution_raw,catalog_year,document"; + +/// Cap on candidate rows fetched while resolving a `--from-db` name. +#[cfg(feature = "database")] +const FROM_DB_MAX_ROWS: usize = 25; + +/// Run `degree analyze --from-db `: fetch one stored program's canonical +/// degree from the database and analyze it with the same options as the +/// file-based path. This is single-program only (no worker pool / `--jobs`). +/// +/// Resolution ladder: exact `program_key`, then exact `degree_id`, then a +/// `name` substring (`ILIKE`). Zero matches exits non-zero; multiple matches +/// print the candidates and exit non-zero; exactly one is parsed from its +/// `document` and analyzed. +#[cfg(feature = "database")] +pub fn run_analyze_from_db(name: &str, options: &AnalyzeOptions, config: &Config) { + use nu_analytics::database::DbClient; + + let Some(rt) = super::db::make_runtime() else { + process::exit(1); + }; + + let client = match rt.block_on(DbClient::from_config(&config.database)) { + Ok(c) => c, + Err(e) => { + eprintln!("✗ Database not available: {e}"); + eprintln!(" Configure the database and run `nuanalytics db login` first."); + process::exit(1); + } + }; + + let rows = match rt.block_on(resolve_program_rows(&client, name)) { + Ok(rows) => rows, + Err(e) => { + eprintln!("✗ Database query failed: {e}"); + process::exit(1); + } + }; + + match rows.len() { + 0 => { + eprintln!("✗ no stored program matches '{name}'"); + process::exit(1); + } + 1 => { + let row = &rows[0]; + let program = match program_from_document(&row.document) { + Ok(p) => p, + Err(e) => { + eprintln!( + "✗ failed to parse stored document for '{}': {e}", + row.program_key + ); + process::exit(1); + } + }; + print_loaded_program(row); + if let Err(e) = analyze_program(&program, options, config) { + eprintln!("Error: {e}"); + process::exit(1); + } + } + n => { + eprintln!( + "⚠ '{name}' matched {n} stored programs — re-run with a more specific name or the exact program_key:" + ); + for line in format_program_candidates(&rows) { + eprintln!(" • {line}"); + } + process::exit(1); + } + } +} + +/// Resolve a `--from-db` name to candidate `programs` rows via the ladder: +/// exact `program_key`, then exact `degree_id`, then `name` substring. +/// +/// Each tier is tried in turn and the first non-empty result wins. The +/// substring tier caps at [`FROM_DB_MAX_ROWS`] so an over-broad name can't pull +/// the whole table. +#[cfg(feature = "database")] +async fn resolve_program_rows( + client: &nu_analytics::database::DbClient, + name: &str, +) -> Result, nu_analytics::database::DatabaseError> { + use nu_analytics::database::{tables, QueryFilters}; + + // Tier 1: exact program_key. + let by_key = QueryFilters::new().eq("program_key", Some(name)); + let rows = parse_program_rows( + &client + .select(tables::PROGRAMS, FROM_DB_COLS, &by_key, Some(1)) + .await?, + ); + if !rows.is_empty() { + return Ok(rows); + } + + // Tier 2: exact degree_id. + let by_id = QueryFilters::new().eq("degree_id", Some(name)); + let rows = parse_program_rows( + &client + .select( + tables::PROGRAMS, + FROM_DB_COLS, + &by_id, + Some(FROM_DB_MAX_ROWS), + ) + .await?, + ); + if !rows.is_empty() { + return Ok(rows); + } + + // Tier 3: name substring (ILIKE '%name%'). + let by_name = QueryFilters::new().ilike("name", Some(name)); + Ok(parse_program_rows( + &client + .select( + tables::PROGRAMS, + FROM_DB_COLS, + &by_name, + Some(FROM_DB_MAX_ROWS), + ) + .await?, + )) +} + +/// Parse a `select` JSON array into [`StoredProgramRow`]s, dropping rows that +/// fail to deserialize. Mirrors the MCP tools' `parse_json_array` pattern. +#[cfg(feature = "database")] +fn parse_program_rows(value: &serde_json::Value) -> Vec { + value + .as_array() + .map(|rows| { + rows.iter() + .filter_map(|r| serde_json::from_value(r.clone()).ok()) + .collect() + }) + .unwrap_or_default() +} + +/// Turn a stored `document` JSONB value into a `DegreeProgram`. +/// +/// The `document` is the lossless unified-degree JSON produced by +/// `to_unified_value` (prerequisites carried in the structured tagged form), so +/// it round-trips through `parse_degree_auto` on its serialized string — the +/// same loader the import core uses on report text. (`serde_json::from_value` +/// would *not* round-trip: the model has no `prerequisites` field, only +/// `prerequisites_raw`.) +#[cfg(feature = "database")] +fn program_from_document( + document: &serde_json::Value, +) -> Result { + let text = serde_json::to_string(document) + .map_err(|e| format!("could not serialize stored document: {e}"))?; + nu_analytics::core::degree::parse_degree_auto(&text) + .map(|(program, _warnings)| program) + .map_err(|e| e.to_string()) +} + +/// Print the "loaded program" banner for a resolved `--from-db` row +/// (name + `program_key` + unitid). +#[cfg(feature = "database")] +fn print_loaded_program(row: &StoredProgramRow) { + let unitid = row + .unitid + .map_or_else(|| "unresolved".to_string(), |u| u.to_string()); + println!( + "✓ Loaded stored program: {} (program_key {} · unitid {unitid})", + row.name, row.program_key + ); +} + +/// Format candidate program rows into one display line each for the ambiguous +/// `--from-db` message: `program_key · name · institution_raw · +/// catalog_year`. Pure (no DB / no I/O) so it can be unit-tested offline. +#[cfg(feature = "database")] +fn format_program_candidates(rows: &[StoredProgramRow]) -> Vec { + rows.iter() + .map(|r| { + let institution = r.institution_raw.as_deref().unwrap_or("(unknown)"); + let catalog = r.catalog_year.as_deref().unwrap_or("(no catalog year)"); + format!( + "{} · {} · {institution} · {catalog}", + r.program_key, r.name + ) + }) + .collect() +} + /// Poll interval for reaping finished worker processes. const WORKER_POLL: std::time::Duration = std::time::Duration::from_millis(50); @@ -1453,13 +1662,29 @@ fn analyze_degree( options: &AnalyzeOptions, config: &Config, ) -> Result { - let verbose = options.verbose; + // Load and validate the degree program, then run the shared single-degree + // analysis on it. This is the in-process, non-worker-pool seam shared with + // the `--from-db` path (which supplies an already-loaded program). + let program = load_degree_program(degree_path, options.verbose)?; + analyze_program(&program, options, config) +} - // Load and validate the degree program - let program = load_degree_program(degree_path, verbose)?; +/// Run the full single-degree analysis on an already-loaded `DegreeProgram`. +/// +/// This is the shared analysis seam: [`analyze_degree`] calls it after loading a +/// file, and the `--from-db` path calls it with a program parsed from a stored +/// `document`. It builds the course graph (breaking cycles), generates plans, +/// computes/aggregates metrics, validates the selected plans, writes the report +/// + CSV outputs, prints the summary, and returns the program rollup. +fn analyze_program( + program: &nu_analytics::core::DegreeProgram, + options: &AnalyzeOptions, + config: &Config, +) -> Result { + let verbose = options.verbose; // Build course graph and handle cycles by breaking them - let mut graph_result = CourseGraph::from_degree_program(&program); + let mut graph_result = CourseGraph::from_degree_program(program); if !graph_result.cycles.is_empty() { if verbose { eprintln!( @@ -1499,8 +1724,8 @@ fn analyze_degree( // Build analysis context let ctx = AnalysisContext { - program: &program, - school: build_school_from_program(&program), + program, + school: build_school_from_program(program), dag: build_dag_from_graph(&graph_result.graph), graph: &graph_result.graph, gen_config: PlanGeneratorConfig { @@ -2517,11 +2742,19 @@ fn expand_courses_with_prerequisites( // - It was added as an OR-alternative for some course // - Another course in the plan would also satisfy that OR requirement // - OR an equivalent course is already in the plan - // BUT never remove protected courses (explicitly included by user) + // BUT only courses ADDED during expansion (Phase 1) may be pruned. Courses + // from the original plan are degree requirements (e.g. a `type: all` core + // course) and must never be dropped here — even when they also happen to be + // an OR-prerequisite alternative for some elective. Without this guard a + // required course like CS320 ("Algorithms"), which is also an OR option of + // an elective's prereq (`CS320 | CS370`), gets deleted whenever the sibling + // CS370 is present, silently dropping it from generated plans. + // `protected_courses` additionally pins user --include courses. + let original_courses: HashSet<&str> = courses.iter().map(String::as_str).collect(); let expanded_clone = expanded.clone(); let redundant = find_redundant_prerequisites(&expanded_clone, graph, equivalences); for course in redundant { - if !protected_courses.contains(&course) { + if !protected_courses.contains(&course) && !original_courses.contains(course.as_str()) { expanded.remove(&course); } } @@ -2944,6 +3177,73 @@ fn print_separator() { mod tests { use super::*; + /// Regression: a required course that is ALSO an OR-prerequisite alternative + /// for another course must survive Phase-2 redundancy pruning in + /// `expand_courses_with_prerequisites`. This mirrors the CSU bug where CS320 + /// (a `type: all` core course, and the `CS320 | CS370` OR-prereq of an + /// elective) was silently dropped from generated plans whenever its OR + /// sibling CS370 was present — only courses ADDED during expansion may be + /// pruned, never the plan's own required courses. + #[test] + fn test_expand_preserves_required_or_alternative() { + let yaml = r#" +degree: + id: regress-or-alt + institution: T + program: T + total_credits: 12 + gpa_minimum: 2.0 +requirements: + core: + name: Core + type: all + category: major + courses: [COR101, COR102] + upper: + name: Upper + type: all + category: major + courses: [UPP301, UPP401] +courses: + COR101: {title: A, prefix: COR, number: "101", credits: 3} + COR102: {title: B, prefix: COR, number: "102", credits: 3} + UPP301: {title: X, prefix: UPP, number: "301", credits: 3, prerequisites_raw: "COR101 | COR102"} + UPP401: {title: Y, prefix: UPP, number: "401", credits: 3, prerequisites_raw: "COR101"} +"#; + let tmp = tempfile::TempDir::new().expect("create tempdir"); + let path = tmp.path().join("degree.yaml"); + std::fs::write(&path, yaml).expect("write temp yaml"); + let program = load_degree_from_yaml(&path).expect("parse degree"); + + let graph = CourseGraph::from_degree_program(&program).graph; + let equivalences = HashMap::new(); + let exclude = HashSet::new(); + let protected = HashSet::new(); + // The plan as the generator produces it: both required core courses plus + // the two required upper courses. + let plan = vec![ + "COR101".to_string(), + "COR102".to_string(), + "UPP301".to_string(), + "UPP401".to_string(), + ]; + let expanded = + expand_courses_with_prerequisites(&plan, &graph, &equivalences, &exclude, &protected); + + // COR102 is an OR-alternative of UPP301's `COR101 | COR102` prereq with no + // other dependents, while COR101 is independently required by UPP401 — so + // pre-fix COR102 was pruned as "redundant". It is a hard requirement and + // must survive expansion. + assert!( + expanded.contains(&"COR102".to_string()), + "required type:all course COR102 was dropped as a redundant OR-prereq: {expanded:?}" + ); + assert!( + expanded.contains(&"COR101".to_string()), + "required core course COR101 must survive expansion: {expanded:?}" + ); + } + #[test] fn test_unique_stem_disambiguates_collisions() { let mut used = HashSet::new(); @@ -3304,4 +3604,113 @@ mod tests { let _ = running[0].1.kill(); let _ = running[0].1.wait(); } + + // --- --from-db helpers (offline; no DB) -------------------------------- + + #[cfg(feature = "database")] + #[test] + fn test_parse_program_rows_skips_malformed() { + // A valid row, a row missing the required `name` field, and a non-object + // — only the valid row should survive. + let value = serde_json::json!([ + { + "program_key": "prog:1|11.0701|2024-2025|BS", + "degree_id": "cs-2024", + "name": "Computer Science", + "unitid": 1, + "institution_raw": "Test U", + "catalog_year": "2024-2025", + "document": {} + }, + { "program_key": "prog:bad", "document": {} }, + "not-an-object" + ]); + let rows = parse_program_rows(&value); + assert_eq!(rows.len(), 1, "only the well-formed row parses"); + assert_eq!(rows[0].program_key, "prog:1|11.0701|2024-2025|BS"); + assert_eq!(rows[0].name, "Computer Science"); + assert_eq!(rows[0].unitid, Some(1)); + } + + #[cfg(feature = "database")] + #[test] + fn test_parse_program_rows_non_array_is_empty() { + assert!(parse_program_rows(&serde_json::json!({})).is_empty()); + assert!(parse_program_rows(&serde_json::Value::Null).is_empty()); + } + + #[cfg(feature = "database")] + #[test] + fn test_format_program_candidates_layout_and_fallbacks() { + let rows = vec![ + StoredProgramRow { + program_key: "prog:1|11.0701|2024-2025|BS".to_string(), + name: "Computer Science".to_string(), + unitid: Some(1), + institution_raw: Some("Test University".to_string()), + catalog_year: Some("2024-2025".to_string()), + document: serde_json::Value::Null, + }, + StoredProgramRow { + program_key: "fp:abcdef".to_string(), + name: "Data Science".to_string(), + unitid: None, + institution_raw: None, + catalog_year: None, + document: serde_json::Value::Null, + }, + ]; + let lines = format_program_candidates(&rows); + assert_eq!( + lines[0], + "prog:1|11.0701|2024-2025|BS · Computer Science · Test University · 2024-2025" + ); + // Missing institution_raw / catalog_year fall back to placeholders. + assert_eq!( + lines[1], + "fp:abcdef · Data Science · (unknown) · (no catalog year)" + ); + } + + #[cfg(feature = "database")] + #[test] + fn test_program_from_document_round_trips_unified_json() { + // A stored `document` carries prerequisites in the structured tagged + // form (what `to_unified_value` emits); parsing it back must recover the + // model — including the `prerequisites_raw` for CS201 → CS101. + let document = serde_json::json!({ + "degree": { + "name": "Computer Science", + "degree_type": "BS", + "system_type": "semester", + "institution": "Test University", + "total_credits": 8 + }, + "requirements": { + "core": { "type": "all", "category": "major", "courses": ["CS101", "CS201"] } + }, + "courses": { + "CS101": { "name": "Intro", "prefix": "CS", "number": "101", "credit_hours": 4.0 }, + "CS201": { + "name": "Data Structures", "prefix": "CS", "number": "201", + "credit_hours": 4.0, "prerequisites": "CS101" + } + } + }); + let program = program_from_document(&document).expect("document should round-trip"); + assert_eq!(program.degree.name, "Computer Science"); + assert_eq!(program.courses.len(), 2); + let cs201 = program.courses.get("CS201").expect("CS201 present"); + assert_eq!(cs201.prerequisites_raw.as_deref(), Some("CS101")); + } + + #[cfg(feature = "database")] + #[test] + fn test_program_from_document_rejects_invalid_document() { + // A JSON object that isn't a degree must error cleanly, never panic. + let bad = serde_json::json!({ "not_a_degree": true }); + let err = program_from_document(&bad) + .expect_err("invalid document must return an error, not panic"); + assert!(!err.is_empty(), "error message should not be empty"); + } } diff --git a/src/cli/main.rs b/src/cli/main.rs index a6f51cc..09c0f9c 100644 --- a/src/cli/main.rs +++ b/src/cli/main.rs @@ -94,6 +94,8 @@ fn run_degree(subcommand: DegreeSubcommand, config: &Config, verbose: bool) { } DegreeSubcommand::Analyze { files, + #[cfg(feature = "database")] + from_db, calc_strategy, sampling_strategy, sample_plans, @@ -122,7 +124,31 @@ fn run_degree(subcommand: DegreeSubcommand, config: &Config, verbose: bool) { jobs, school, }; - commands::degree::run_analyze(&files, &options, config); + // Exactly one of {files, --from-db} must be provided. The DB path + // is single-program (no worker pool); the file path is unchanged. + #[cfg(feature = "database")] + match (files.is_empty(), from_db) { + (true, Some(name)) => { + commands::degree::run_analyze_from_db(&name, &options, config); + } + (false, None) => commands::degree::run_analyze(&files, &options, config), + (true, None) => { + eprintln!("Error: provide degree file(s) or --from-db (got neither)."); + std::process::exit(2); + } + (false, Some(_)) => { + eprintln!("Error: pass either degree file(s) or --from-db , not both."); + std::process::exit(2); + } + } + #[cfg(not(feature = "database"))] + { + if files.is_empty() { + eprintln!("Error: No degree file specified."); + std::process::exit(2); + } + commands::degree::run_analyze(&files, &options, config); + } } DegreeSubcommand::Trim { files, diff --git a/src/core/database/import.rs b/src/core/database/import.rs new file mode 100644 index 0000000..6217849 --- /dev/null +++ b/src/core/database/import.rs @@ -0,0 +1,1522 @@ +//! Shared "degree import" core. +//! +//! Loads a degree-first analysis report (or a plain unified degree) into the +//! normalized program tables (`programs`, `courses`, `program_courses`, +//! `program_requirements`) plus, when the input carries an `analysis` block, one +//! analysis run (`analysis_runs`, `analysis_course_metrics`, `analysis_plans`). +//! +//! The flow is split into a **pure builder** ([`build_import_plan`]) that turns a +//! report text into row structs without touching the database (used by +//! `--dry-run`), and an **async executor** ([`execute_import`]) that resolves the +//! institution, performs the existence/verified decision, and upserts the rows +//! (children first, the `programs` commit marker last). +//! +//! See `docs/database/programs-schema.sql` and the approved design plan for the +//! exact semantics this module follows. + +use serde_json::Value; +use sha2::{Digest, Sha256}; + +use super::error::{DatabaseError, DatabaseResult}; +use super::models::{ + StoredAnalysisCourseMetric, StoredAnalysisPlan, StoredAnalysisRun, StoredCourse, StoredProgram, + StoredProgramCourse, StoredProgramRequirement, +}; +use super::query::QueryFilters; +use super::tables; +use super::DbClient; +use crate::core::degree::{parse_degree_auto, serialize_degree_json, to_unified_value}; +use crate::core::models::degree::{Requirement, RequirementType}; +use crate::core::models::{Course, DegreeProgram}; +use crate::core::prerequisite_parser::parse_to_ast; + +/// Stable sha256 content hash (lowercase hex). +/// +/// Used for persisted hashes (`document_hash`, `run_key`, fingerprint program +/// keys) — `DefaultHasher` is intentionally avoided because it is not stable +/// across Rust versions. +#[must_use] +pub fn content_hash(s: &str) -> String { + let digest = Sha256::digest(s.as_bytes()); + let mut out = String::with_capacity(digest.len() * 2); + for byte in digest { + use std::fmt::Write as _; + // Writing hex into a String never fails. + let _ = write!(out, "{byte:02x}"); + } + out +} + +// --- safe numeric conversions for projecting model fields into DB rows ------ +// Degree/Course credit fields are small `u32`/`usize` counts and `f64` metric +// stats; these helpers convert into the DB column widths (`i32`/`f32`/`i64`) +// without lossy-cast lints, saturating on the (practically unreachable) +// out-of-range case. + +/// `u32` count → `i32`, saturating at `i32::MAX`. +fn u32_to_i32(v: u32) -> i32 { + i32::try_from(v).unwrap_or(i32::MAX) +} + +/// `usize` count → `i32`, saturating at `i32::MAX`. +fn usize_to_i32(v: usize) -> i32 { + i32::try_from(v).unwrap_or(i32::MAX) +} + +/// JSON `i64` → `i32`, saturating into range. +fn i64_to_i32(v: i64) -> i32 { + i32::try_from(v).unwrap_or(if v < 0 { i32::MIN } else { i32::MAX }) +} + +/// `u128` epoch-ms → `i64`, saturating at `i64::MAX`. +fn u128_to_i64(v: u128) -> i64 { + i64::try_from(v).unwrap_or(i64::MAX) +} + +/// JSON `f64` metric stat → `f32` (the DB column width). The narrowing is +/// intentional; metric means fit comfortably in `f32`. +#[allow(clippy::cast_possible_truncation)] +const fn f64_to_f32(v: f64) -> f32 { + v as f32 +} + +/// Options controlling a single degree import. +// +// The four boolean flags (`force`/`replace`/`skip_existing`/`dry_run`) map +// 1:1 onto the CLI/MCP flags this options struct mirrors; folding them into an +// enum would obscure that direct correspondence and break callers that set them +// independently, so the `struct_excessive_bools` lint is allowed here by design. +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone)] +pub struct ImportOptions { + /// Analysis-run variant label (default `"full"`). `full` is the canonical + /// upload that writes the program projection; non-`full` only attaches a run. + pub variant: String, + /// Override the resolved institution unit id. + pub unitid: Option, + /// Override the institution name. + pub institution: Option, + /// Override the CIP code (part of the natural program key). + pub cip_code: Option, + /// Override the catalog year (part of the program identity). + pub catalog_year: Option, + /// Override the degree id. + pub degree_id: Option, + /// Overwrite a verified program / skip confirmation. + pub force: bool, + /// Replace an existing (unverified) program. + pub replace: bool, + /// Skip the program entirely if it already exists. + pub skip_existing: bool, + /// Build the plan but write nothing. + pub dry_run: bool, +} + +impl Default for ImportOptions { + fn default() -> Self { + Self { + variant: "full".to_string(), + unitid: None, + institution: None, + cip_code: None, + catalog_year: None, + degree_id: None, + force: false, + replace: false, + skip_existing: false, + dry_run: false, + } + } +} + +/// Outcome class for one import. +#[derive(Debug, Clone)] +pub enum ImportResult { + /// The program did not exist and was created. + Created, + /// The program existed and was overwritten. + Updated, + /// The program existed and was left untouched. + Skipped, + /// The program exists and overwriting requires explicit confirmation. + /// Carries a human-readable reason. + NeedsConfirmation(String), + /// The institution name matched several institutions; the caller must pick + /// one and re-run with an explicit `unitid`. Carries `(unitid, name)` pairs. + InstitutionAmbiguous(Vec<(i32, String)>), + /// The report could not be turned into a valid plan. Carries the errors. + Rejected(Vec), +} + +/// The full result of one import (counts + diagnostics). +#[derive(Debug, Clone)] +pub struct ImportOutcome { + /// The deterministic program key this import resolved to. + pub program_key: String, + /// What happened to the program. + pub result: ImportResult, + /// The resolved IPEDS unit id, when one was determined. + pub resolved_unitid: Option, + /// The analysis-run variant label. + pub variant: String, + /// Institution name from the degree (raw), for display. + pub institution: Option, + /// Plan variations enumerated/sampled by the analysis run; `None` when the + /// upload carried no metrics (a degree-only upload). + pub variations_run: Option, + /// Plan sampling strategy used by the analysis run, if any. + pub sample_type: Option, + /// Number of `courses` rows written. + pub courses_written: usize, + /// Number of `program_requirements` rows written. + pub requirements_written: usize, + /// Whether an `analysis_runs` row was written. + pub run_written: bool, + /// Number of `analysis_plans` rows written. + pub plans_written: usize, + /// Number of `analysis_course_metrics` rows written. + pub course_metrics_written: usize, + /// Warnings surfaced while parsing/converting the degree. + pub conversion_warnings: Vec, + /// Informational messages about decisions taken. + pub messages: Vec, +} + +/// A fully-built set of rows ready to write — produced without touching the DB. +#[derive(Debug, Clone)] +pub struct ImportPlan { + /// The program row (always populated; the executor decides whether to write + /// it based on [`Self::is_full`] and the existence decision). + pub program: Option, + /// Whether the variant is the canonical `full` upload. + pub is_full: bool, + /// Shared course catalog rows. + pub courses: Vec, + /// Program↔course junction rows. + pub program_courses: Vec, + /// Flattened requirement-tree rows. + pub requirements: Vec, + /// The analysis run, when the report carried an `analysis` block. + pub run: Option, + /// Per-course analysis metric rows. + pub course_metrics: Vec, + /// Selected-plan rows. + pub plans: Vec, +} + +/// Normalize an institution name into a stable slug: +/// lowercase, trimmed, non-alphanumeric → `-`, collapsed dashes, no leading/ +/// trailing dash. +#[must_use] +pub fn institution_slug(name: &str) -> String { + let mut out = String::with_capacity(name.len()); + let mut last_dash = true; // suppress leading dashes + for ch in name.trim().to_lowercase().chars() { + if ch.is_ascii_alphanumeric() { + out.push(ch); + last_dash = false; + } else if !last_dash { + out.push('-'); + last_dash = true; + } + } + while out.ends_with('-') { + out.pop(); + } + out +} + +/// Compute the `institution_ref` (course-catalog partition key). +fn institution_ref(resolved_unitid: Option, institution: Option<&str>) -> String { + resolved_unitid.map_or_else( + || institution.map_or_else(|| "unknown".to_string(), institution_slug), + |u| u.to_string(), + ) +} + +/// Compute the deterministic `program_key` for a degree. +fn program_key( + degree: &crate::core::models::Degree, + opts: &ImportOptions, + resolved_unitid: Option, +) -> String { + let cip = opts + .cip_code + .clone() + .or_else(|| degree.cip_code.clone()) + .unwrap_or_default(); + let cat = opts + .catalog_year + .clone() + .or_else(|| degree.catalog_year.clone()) + .unwrap_or_default(); + let dtype = °ree.degree_type; + if let Some(u) = resolved_unitid { + format!("prog:{u}|{cip}|{cat}|{dtype}") + } else if let Some(id) = opts.degree_id.clone().or_else(|| degree.id.clone()) { + format!("prog:{id}|{cat}") + } else { + let institution = degree.institution.clone().unwrap_or_default(); + let source_url = degree.source_url.clone().unwrap_or_default(); + let fp = [ + institution, + degree.name.clone(), + dtype.clone(), + cat, + source_url, + ] + .join("|"); + format!("fp:{}", content_hash(&fp)) + } +} + +/// Derive the normalized `program_kind` from degree type, tags, and name. +fn derive_program_kind(degree: &crate::core::models::Degree) -> String { + let kinds = [ + "major", + "minor", + "concentration", + "certificate", + "track", + "specialization", + "emphasis", + "micro", + ]; + let haystack = format!( + "{} {} {}", + degree.degree_type.to_lowercase(), + degree.name.to_lowercase(), + degree + .tags + .as_ref() + .map(|t| t.join(" ").to_lowercase()) + .unwrap_or_default() + ); + for kind in kinds { + if haystack.contains(kind) { + return kind.to_string(); + } + } + "major".to_string() +} + +/// Derive the primary `discipline` from the degree tags (first of ai/cs/ds/cy). +fn derive_discipline(degree: &crate::core::models::Degree) -> Option { + let tags = degree.tags.as_ref()?; + let lower: Vec = tags.iter().map(|t| t.to_lowercase()).collect(); + for disc in ["ai", "cs", "ds", "cy"] { + if lower.iter().any(|t| t == disc) { + return Some(disc.to_string()); + } + } + None +} + +/// Pull the `mean` of a named metric stat block out of a metrics `Value`. +fn metric_mean(metrics: &Value, key: &str) -> Option { + metrics + .get(key) + .and_then(|m| m.get("mean")) + .and_then(Value::as_f64) + .map(f64_to_f32) +} + +/// Build the structured prerequisite tree for a course from its raw expression. +fn course_prereq_value(course: &Course) -> Option { + let raw = course.prerequisites_raw.as_deref()?; + let expr = parse_to_ast(raw)?; + serde_json::to_value(&expr).ok() +} + +/// Build a [`StoredCourse`] from a parsed [`Course`]. +fn build_course( + course: &Course, + course_code: &str, + institution_ref: &str, + resolved_unitid: Option, + generation: i64, +) -> StoredCourse { + let (credit_min, credit_max) = course.credit_range.as_ref().map_or((None, None), |r| { + (Some(u32_to_i32(r.min)), Some(u32_to_i32(r.max))) + }); + StoredCourse { + id: None, + institution_ref: institution_ref.to_string(), + unitid: resolved_unitid, + course_code: course_code.to_string(), + prefix: (!course.prefix.is_empty()).then(|| course.prefix.clone()), + number: (!course.number.is_empty()).then(|| course.number.clone()), + name: (!course.name.is_empty()).then(|| course.name.clone()), + credit_hours: Some(course.credit_hours), + credit_min, + credit_max, + prerequisites: course_prereq_value(course), + prerequisites_raw: course.prerequisites_raw.clone(), + gen_ed_attributes: course.gen_ed_attributes.clone(), + cross_listed_as: course.cross_listed_as.clone(), + tags: course.tags.clone(), + generation, + created_at: None, + updated_at: None, + } +} + +/// Derive the requirement's effective double-count flag. +/// +/// `exclude_used = true` → no double counting (`Some(false)`). +/// `exclude_used = false` → double counting allowed (`Some(true)`). +/// unset → fall back to the program's global `allow_double_counting` default. +fn allow_double_count(req: &Requirement, program_default: Option) -> Option { + match req.constraints.as_ref().and_then(|c| c.exclude_used) { + Some(true) => Some(false), + Some(false) => Some(true), + None => program_default, + } +} + +/// Whether a `select` requirement is logically impossible (`count` exceeds the +/// known explicit pool size). Only an explicit `from.courses` list yields a +/// known pool; patterns/groups → unknown → not impossible. +fn requirement_is_impossible(req: &Requirement) -> bool { + if req.req_type != RequirementType::Select { + return false; + } + let Some(count) = req.count else { + return false; + }; + req.from + .as_ref() + .and_then(|f| f.courses.as_ref()) + .is_some_and(|pool| u64::from(count) > pool.len() as u64) +} + +/// Map a [`RequirementType`] to its DB string. +const fn req_type_str(t: &RequirementType) -> &'static str { + match t { + RequirementType::All => "all", + RequirementType::Select => "select", + RequirementType::OneOf => "one_of", + } +} + +/// Per-program context threaded through the requirement-tree walk (the bits +/// that stay constant across every node). +struct ReqContext<'a> { + program_key: &'a str, + /// Program-level `allow_double_counting` default. + program_default: Option, + generation: i64, +} + +/// The address of one requirement node in the flattened tree. +struct ReqAddress<'a> { + req_path: &'a str, + parent_path: Option<&'a str>, + map_key: Option<&'a str>, + option_id: Option<&'a str>, + option_name: Option<&'a str>, +} + +/// Build a single [`StoredProgramRequirement`] row from a [`Requirement`]. +fn build_requirement_row( + req: &Requirement, + ctx: &ReqContext, + addr: &ReqAddress, +) -> StoredProgramRequirement { + let (credit_min, credit_max) = req.credit_range.as_ref().map_or((None, None), |r| { + (Some(u32_to_i32(r.min)), Some(u32_to_i32(r.max))) + }); + StoredProgramRequirement { + id: None, + program_key: ctx.program_key.to_string(), + req_path: addr.req_path.to_string(), + parent_path: addr.parent_path.map(str::to_string), + map_key: addr.map_key.map(str::to_string), + option_id: addr.option_id.map(str::to_string), + option_name: addr.option_name.map(str::to_string), + name: req.name.clone(), + req_type: req_type_str(&req.req_type).to_string(), + category: req.category.clone(), + count: req.count.map(u32_to_i32), + credits: req.credits.map(u32_to_i32), + credit_min, + credit_max, + tags: req.tags.clone(), + courses: req + .courses + .as_ref() + .and_then(|c| serde_json::to_value(c).ok()), + selection_spec: req.from.as_ref().and_then(|f| serde_json::to_value(f).ok()), + req_constraints: req + .constraints + .as_ref() + .and_then(|c| serde_json::to_value(c).ok()), + is_impossible: requirement_is_impossible(req), + allow_double_count: allow_double_count(req, ctx.program_default), + generation: ctx.generation, + } +} + +/// Recursively flatten a requirement (and any `one_of` options) into rows. +fn flatten_requirement( + req: &Requirement, + ctx: &ReqContext, + addr: &ReqAddress, + out: &mut Vec, +) { + out.push(build_requirement_row(req, ctx, addr)); + + if req.req_type == RequirementType::OneOf { + if let Some(options) = req.options.as_ref() { + for option in options { + for (j, nested) in option.requirements.iter().enumerate() { + let nested_path = format!("{}#{}#{j}", addr.req_path, option.id); + flatten_requirement( + nested, + ctx, + &ReqAddress { + req_path: &nested_path, + parent_path: Some(addr.req_path), + map_key: None, + option_id: Some(&option.id), + option_name: Some(&option.name), + }, + out, + ); + } + } + } + } +} + +/// Build the full set of rows for an import without touching the database. +/// +/// `resolved_unitid` and `generation` are supplied by the executor (or a test). +/// +/// # Errors +/// Returns [`DatabaseError::ParseError`] when the report text cannot be parsed +/// as a degree or serialized back to the canonical document. +// Top-level sequential assembly of one `ImportPlan`. The sub-steps already +// delegate to helpers (`build_course`, `flatten_requirement`, `build_analysis_run`); +// the remaining length is the flat field-by-field `StoredProgram` mapping, which +// reads clearer inline than behind a 9-field args struct — so the line-count lint +// is allowed here by design. +#[allow(clippy::too_many_lines)] +pub fn build_import_plan( + report_text: &str, + opts: &ImportOptions, + resolved_unitid: Option, + generation: i64, +) -> Result<(ImportPlan, Vec), DatabaseError> { + // (a) canonical degree (ignores analysis / metrics / selected_plans blocks) + let (program, conversion_warnings) = parse_degree_auto(report_text) + .map_err(|e| DatabaseError::ParseError(format!("failed to parse degree: {e}")))?; + // (b) raw report value for the report-only blocks + let raw: Value = serde_json::from_str(report_text).unwrap_or(Value::Null); + + let degree = &program.degree; + let variant = opts.variant.clone(); + let is_full = variant.eq_ignore_ascii_case("full"); + + let inst_ref = institution_ref(resolved_unitid, degree.institution.as_deref()); + let program_key = program_key(degree, opts, resolved_unitid); + + let document = to_unified_value(&program) + .map_err(|e| DatabaseError::ParseError(format!("failed to build document: {e}")))?; + let document_hash = content_hash( + &serialize_degree_json(&program, false) + .map_err(|e| DatabaseError::ParseError(format!("failed to serialize degree: {e}")))?, + ); + + // ---- courses + program_courses -------------------------------------- + let mut courses = Vec::with_capacity(program.courses.len()); + let mut program_courses = Vec::with_capacity(program.courses.len()); + let mut course_keys: Vec<&String> = program.courses.keys().collect(); + course_keys.sort(); // deterministic order + for key in &course_keys { + let course = &program.courses[*key]; + courses.push(build_course( + course, + key, + &inst_ref, + resolved_unitid, + generation, + )); + program_courses.push(StoredProgramCourse { + id: None, + program_key: program_key.clone(), + institution_ref: inst_ref.clone(), + course_code: (*key).clone(), + credit_hours_override: None, + name_as_listed: None, + generation, + }); + } + + // ---- requirements (recursive, deterministic by map key) ------------- + let mut requirements = Vec::new(); + let mut req_keys: Vec<&String> = program.requirements.keys().collect(); + req_keys.sort(); + let req_ctx = ReqContext { + program_key: &program_key, + program_default: degree.allow_double_counting, + generation, + }; + for key in &req_keys { + let req = &program.requirements[*key]; + flatten_requirement( + req, + &req_ctx, + &ReqAddress { + req_path: key, + parent_path: None, + map_key: Some(key), + option_id: None, + option_name: None, + }, + &mut requirements, + ); + } + + let has_impossible = requirements.iter().any(|r| r.is_impossible); + + // ---- program row (always built; executor decides whether to write) -- + let stored_program = StoredProgram { + id: None, + program_key: program_key.clone(), + degree_id: opts.degree_id.clone().or_else(|| degree.id.clone()), + unitid: resolved_unitid, + institution_ref: inst_ref, + institution_raw: opts + .institution + .clone() + .or_else(|| degree.institution.clone()), + cip_code: opts.cip_code.clone().or_else(|| degree.cip_code.clone()), + name: degree.name.clone(), + degree_type: (!degree.degree_type.is_empty()).then(|| degree.degree_type.clone()), + program_kind: Some(derive_program_kind(degree)), + discipline: derive_discipline(degree), + system_type: degree.system_type.clone(), + tags: degree.tags.clone(), + catalog_year: opts + .catalog_year + .clone() + .or_else(|| degree.catalog_year.clone()), + source_url: degree.source_url.clone(), + total_credits: degree.total_credits.map(u32_to_i32), + upper_division_credits: degree.upper_division_credits.map(u32_to_i32), + in_major_credits: degree.in_major_credits.map(u32_to_i32), + gpa_minimum: degree.gpa_minimum, + gpa_major: degree.gpa_major, + grade_minimum: degree.grade_minimum.clone(), + major_subjects: degree.major_subjects.clone(), + allow_double_counting: degree.allow_double_counting, + document: document.clone(), + document_hash: document_hash.clone(), + verified: false, + institution_resolved: resolved_unitid.is_some(), + has_impossible_requirements: has_impossible, + generation, + created_at: None, + updated_at: None, + }; + + // ---- analysis run + children (only when report has `analysis`) ------ + let (run, course_metrics, plans) = match raw.get("analysis").filter(|v| !v.is_null()) { + Some(analysis) => { + let run = build_analysis_run(BuildRunArgs { + analysis, + program: &program, + program_key: &program_key, + variant: &variant, + is_full, + document, + document_hash, + generation, + })?; + let course_metrics = build_course_metrics(&raw, &run.run_key, &program_key, generation); + let plans = build_plans(&raw, &run.run_key, &program_key, generation); + (Some(run), course_metrics, plans) + } + None => (None, Vec::new(), Vec::new()), + }; + + let plan = ImportPlan { + program: Some(stored_program), + is_full, + courses, + program_courses, + requirements, + run, + course_metrics, + plans, + }; + Ok((plan, conversion_warnings)) +} + +/// Inputs for [`build_analysis_run`] (grouped so the builder stays under the +/// argument-count limit). +struct BuildRunArgs<'a> { + analysis: &'a Value, + program: &'a DegreeProgram, + program_key: &'a str, + variant: &'a str, + is_full: bool, + /// The canonical document (consumed for a non-full variant's `analyzed_document`). + document: Value, + /// The canonical document hash (reused for a full run). + document_hash: String, + generation: i64, +} + +/// Build the [`StoredAnalysisRun`] from a report's `analysis` block. +fn build_analysis_run(args: BuildRunArgs) -> Result { + let BuildRunArgs { + analysis, + program, + program_key, + variant, + is_full, + document, + document_hash, + generation, + } = args; + + let variations_run = analysis + .get("variations_run") + .and_then(Value::as_i64) + .map(i64_to_i32); + let sample_type = analysis + .get("sample_type") + .and_then(Value::as_str) + .map(str::to_string); + let degree_metrics = analysis.get("metrics").cloned(); + + // For non-full variants the analyzed artifact differs from the program + // document, so we record this variant's own hash + the document value. + let analyzed_document_hash = + if is_full { + document_hash + } else { + content_hash(&serialize_degree_json(program, false).map_err(|e| { + DatabaseError::ParseError(format!("failed to serialize variant: {e}")) + })?) + }; + let analyzed_document = if is_full { None } else { Some(document) }; + + let run_key = content_hash(&format!( + "{program_key}|{analyzed_document_hash}|{variant}|{}|{}|{}|{}|{}|{}", + variations_run.map(|v| v.to_string()).unwrap_or_default(), + sample_type.clone().unwrap_or_default(), + "", // calc_strategy (None for now) + "", // max_plans (None for now) + "", // full_run (None for now) + "", // include_joined (None for now) + )); + + let (complexity_mean, delay_mean, credits_mean) = + degree_metrics.as_ref().map_or((None, None, None), |m| { + ( + metric_mean(m, "complexity"), + metric_mean(m, "delay"), + metric_mean(m, "credits"), + ) + }); + + Ok(StoredAnalysisRun { + id: None, + run_key, + program_key: program_key.to_string(), + analyzed_document_hash, + variant: variant.to_string(), + trimmed: !is_full && variant.eq_ignore_ascii_case("trimmed"), + analyzed_document, + variations_run, + sample_type, + calc_strategy: None, + sampling_strategy: None, + max_plans: None, + full_run: None, + included_courses: None, + degree_metrics, + complexity_mean, + delay_mean, + credits_mean, + generation, + created_at: None, + updated_at: None, + }) +} + +/// Build per-course metric rows from the raw report's `courses.*.metrics`. +fn build_course_metrics( + raw: &Value, + run_key: &str, + program_key: &str, + generation: i64, +) -> Vec { + let Some(courses_obj) = raw.get("courses").and_then(Value::as_object) else { + return Vec::new(); + }; + let mut metric_keys: Vec<&String> = courses_obj.keys().collect(); + metric_keys.sort(); + let mut out = Vec::new(); + for code in metric_keys { + let Some(metrics) = courses_obj[code].get("metrics").filter(|v| !v.is_null()) else { + continue; + }; + let plan_count = metrics + .get("plan_count") + .and_then(Value::as_i64) + .map(i64_to_i32); + // Drop the redundant course_id before storing. + let mut metrics_value = metrics.clone(); + if let Some(obj) = metrics_value.as_object_mut() { + obj.remove("course_id"); + } + out.push(StoredAnalysisCourseMetric { + id: None, + run_key: run_key.to_string(), + program_key: program_key.to_string(), + course_code: code.clone(), + plan_count, + complexity_mean: metric_mean(metrics, "complexity"), + centrality_mean: metric_mean(metrics, "centrality"), + delay_mean: metric_mean(metrics, "delay"), + blocking_mean: metric_mean(metrics, "blocking"), + metrics: Some(metrics_value), + generation, + }); + } + out +} + +/// Build selected-plan rows from the raw report's `selected_plans[]`. +fn build_plans( + raw: &Value, + run_key: &str, + program_key: &str, + generation: i64, +) -> Vec { + let Some(selected) = raw.get("selected_plans").and_then(Value::as_array) else { + return Vec::new(); + }; + selected + .iter() + .enumerate() + .map(|(i, plan)| StoredAnalysisPlan { + id: None, + run_key: run_key.to_string(), + program_key: program_key.to_string(), + plan_index: usize_to_i32(i), + category: plan + .get("category") + .and_then(Value::as_str) + .map(str::to_string), + terms_required: plan + .get("terms_required") + .and_then(Value::as_i64) + .map(i64_to_i32), + total_complexity: plan + .get("total_complexity") + .and_then(Value::as_f64) + .map(f64_to_f32), + longest_delay: plan + .get("longest_delay") + .and_then(Value::as_f64) + .map(f64_to_f32), + credits: plan.get("credits").and_then(Value::as_f64).map(f64_to_f32), + course_count: plan + .get("course_count") + .and_then(Value::as_i64) + .map(i64_to_i32), + is_calc_ready: plan.get("is_calc_ready").and_then(Value::as_bool), + critical_path: plan.get("critical_path").cloned(), + schedule: plan.get("schedule").cloned(), + generation, + }) + .collect() +} + +/// Current epoch time in milliseconds (the per-import `generation` stamp). +fn now_generation() -> i64 { + use std::time::{SystemTime, UNIX_EPOCH}; + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_or(0, |d| u128_to_i64(d.as_millis())) +} + +/// Snapshot of an existing program's idempotency fields. +#[derive(Debug, serde::Deserialize)] +struct ExistingProgram { + document_hash: String, + verified: bool, +} + +/// Resolve the institution → unit id. +/// +/// Returns `Ok(Some(unitid))` on a confident resolution, `Ok(None)` when the +/// institution stays unresolved, and `Err(candidates)` when the name matched +/// several institutions (the caller must disambiguate). +async fn resolve_institution( + client: &DbClient, + opts: &ImportOptions, + degree: &crate::core::models::Degree, +) -> DatabaseResult, Vec<(i32, String)>>> { + if let Some(u) = opts.unitid { + return Ok(Ok(Some(u))); + } + if let Some(u) = degree.unitid { + return Ok(Ok(Some(u))); + } + let Some(name) = opts + .institution + .as_deref() + .or(degree.institution.as_deref()) + else { + return Ok(Ok(None)); + }; + let filters = QueryFilters::new().ilike("name", Some(name)); + let value = client + .select( + tables::INSTITUTIONS, + "unitid,name,state", + &filters, + Some(10), + ) + .await?; + let rows = value.as_array().cloned().unwrap_or_default(); + let candidates: Vec<(i32, String)> = rows + .iter() + .filter_map(|r| { + let u = i64_to_i32(r.get("unitid").and_then(Value::as_i64)?); + let n = r + .get("name") + .and_then(Value::as_str) + .unwrap_or_default() + .to_string(); + Some((u, n)) + }) + .collect(); + match candidates.len() { + 0 => Ok(Ok(None)), + 1 => Ok(Ok(Some(candidates[0].0))), + _ => Ok(Err(candidates)), + } +} + +/// Import a degree report into the database. +/// +/// Resolves the institution, computes the existence/verified decision, builds +/// the row plan, and (unless `dry_run`) upserts the rows — children first, the +/// `programs` commit marker last. +/// +/// Domain results (skipped, needs-confirmation, ambiguous, rejected) are +/// returned as `Ok(ImportOutcome)`; only transport/parse failures are `Err`. +/// +/// # Errors +/// Returns [`DatabaseError`] when a query/upsert fails or the report cannot be +/// parsed. +pub async fn execute_import( + client: &DbClient, + report_text: &str, + opts: &ImportOptions, +) -> DatabaseResult { + // Parse once up front so we can resolve the institution before building. + let (program, _warn) = parse_degree_auto(report_text) + .map_err(|e| DatabaseError::ParseError(format!("failed to parse degree: {e}")))?; + let degree = program.degree.clone(); + + // 1. institution resolution. + let resolved_unitid = match resolve_institution(client, opts, °ree).await? { + Ok(u) => u, + Err(candidates) => { + let key = program_key(°ree, opts, None); + return Ok(ImportOutcome { + program_key: key, + result: ImportResult::InstitutionAmbiguous(candidates), + resolved_unitid: None, + variant: opts.variant.clone(), + institution: opts + .institution + .clone() + .or_else(|| degree.institution.clone()), + variations_run: None, + sample_type: None, + courses_written: 0, + requirements_written: 0, + run_written: false, + plans_written: 0, + course_metrics_written: 0, + conversion_warnings: Vec::new(), + messages: vec!["institution name matched multiple institutions".to_string()], + }); + } + }; + + // 2. generation. + let generation = now_generation(); + + // 3. build the plan. + let (plan, conversion_warnings) = + build_import_plan(report_text, opts, resolved_unitid, generation)?; + let program_key = plan + .program + .as_ref() + .map(|p| p.program_key.clone()) + .unwrap_or_default(); + + // 4. existence decision. + let document_hash = plan + .program + .as_ref() + .map(|p| p.document_hash.clone()) + .unwrap_or_default(); + let existing = fetch_existing_program(client, &program_key).await?; + let decision = decide_existence(existing.as_ref(), &document_hash, plan.is_full, opts); + + let mut outcome = ImportOutcome { + program_key: program_key.clone(), + result: decision.result, + resolved_unitid, + variant: opts.variant.clone(), + institution: opts + .institution + .clone() + .or_else(|| degree.institution.clone()), + variations_run: plan.run.as_ref().and_then(|r| r.variations_run), + sample_type: plan.run.as_ref().and_then(|r| r.sample_type.clone()), + courses_written: plan.courses.len(), + requirements_written: plan.requirements.len(), + run_written: plan.run.is_some(), + plans_written: plan.plans.len(), + course_metrics_written: plan.course_metrics.len(), + conversion_warnings, + messages: decision.messages, + }; + + // 5. dry-run: report counts, write nothing. + if opts.dry_run { + outcome + .messages + .push("dry-run: nothing written".to_string()); + return Ok(outcome); + } + + // 6. writes — children first, run-parents as commit markers, program last. + write_import_plan( + client, + plan, + decision.write_program, + decision.write_minimal_program, + &mut outcome, + ) + .await?; + + Ok(outcome) +} + +/// Fetch an existing program's idempotency snapshot, if any. +async fn fetch_existing_program( + client: &DbClient, + program_key: &str, +) -> DatabaseResult> { + let filters = QueryFilters::new().eq("program_key", Some(program_key)); + let value = client + .select( + tables::PROGRAMS, + "program_key,document_hash,verified,generation", + &filters, + Some(1), + ) + .await?; + Ok(value + .as_array() + .and_then(|a| a.first()) + .and_then(|v| serde_json::from_value(v.clone()).ok())) +} + +/// The outcome of the existence/verified decision. +struct ExistenceDecision { + result: ImportResult, + /// Whether to upsert the canonical (full) programs row. + write_program: bool, + /// Whether to upsert a minimal flagged programs row (non-full, missing). + write_minimal_program: bool, + messages: Vec, +} + +/// Decide what to do with an existing program. `write_program` gates whether we +/// upsert the canonical programs row; the run/metrics are always allowed even +/// when the program itself is skipped. +fn decide_existence( + existing: Option<&ExistingProgram>, + document_hash: &str, + is_full: bool, + opts: &ImportOptions, +) -> ExistenceDecision { + let mut messages = Vec::new(); + let (result, write_program) = match existing { + None => (ImportResult::Created, is_full), + Some(_) if opts.skip_existing => { + messages.push("program exists; --skip-existing set".to_string()); + (ImportResult::Skipped, false) + } + Some(e) if e.document_hash == document_hash => { + messages.push("document unchanged; skipping program".to_string()); + (ImportResult::Skipped, false) + } + Some(e) if e.verified && !opts.force => ( + ImportResult::NeedsConfirmation( + "verified program; pass --force to overwrite".to_string(), + ), + false, + ), + Some(_) if !opts.force && !opts.replace => ( + ImportResult::NeedsConfirmation( + "program exists; pass --replace/--force to overwrite".to_string(), + ), + false, + ), + Some(_) => (ImportResult::Updated, is_full), + }; + // For a non-full variant whose program is missing, write a minimal flagged + // program row so the run isn't orphaned. + let write_minimal_program = !is_full && existing.is_none(); + ExistenceDecision { + result, + write_program, + write_minimal_program, + messages, + } +} + +/// Upsert the plan's rows: children first, the analysis run after its children, +/// then the programs row as the commit marker. Updates `outcome` counts when a +/// projection/program write is skipped. +async fn write_import_plan( + client: &DbClient, + plan: ImportPlan, + write_program: bool, + write_minimal_program: bool, + outcome: &mut ImportOutcome, +) -> DatabaseResult<()> { + // Projection rows: only on a canonical (full) create/update. + if plan.is_full + && matches!( + outcome.result, + ImportResult::Created | ImportResult::Updated + ) + { + client + .upsert_batch( + tables::COURSES, + plan.courses, + &["institution_ref", "course_code"], + ) + .await?; + client + .upsert_batch( + tables::PROGRAM_COURSES, + plan.program_courses, + &["program_key", "course_code"], + ) + .await?; + client + .upsert_batch( + tables::PROGRAM_REQUIREMENTS, + plan.requirements, + &["program_key", "req_path"], + ) + .await?; + } else { + // Non-full variant or skipped program: don't touch the projection rows. + outcome.courses_written = 0; + outcome.requirements_written = 0; + } + + // Analysis run + children (always allowed when present): children first. + if let Some(run) = plan.run { + if !plan.course_metrics.is_empty() { + client + .upsert_batch( + tables::ANALYSIS_COURSE_METRICS, + plan.course_metrics, + &["run_key", "course_code"], + ) + .await?; + } + if !plan.plans.is_empty() { + client + .upsert_batch( + tables::ANALYSIS_PLANS, + plan.plans, + &["run_key", "plan_index"], + ) + .await?; + } + client + .upsert_batch(tables::ANALYSIS_RUNS, vec![run], &["run_key"]) + .await?; + } + + // Program row (commit marker) LAST. Full canonical write, or a minimal + // flagged row for a non-full variant whose program does not yet exist. + if let Some(program_row) = plan.program { + if write_program { + client + .upsert_batch(tables::PROGRAMS, vec![program_row], &["program_key"]) + .await?; + } else if write_minimal_program { + client + .upsert_batch(tables::PROGRAMS, vec![program_row], &["program_key"]) + .await?; + outcome + .messages + .push("created minimal program row for non-full variant".to_string()); + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn opts() -> ImportOptions { + ImportOptions::default() + } + + /// A minimal full report with `one_of` options, analysis, course metrics, and + /// `selected_plans` — exercises every builder path. + fn sample_report() -> String { + r#"{ + "degree": { + "name": "Computer Science", + "degree_type": "BS", + "system_type": "semester", + "institution": "Test University", + "catalog_year": "2024-2025", + "cip_code": "11.0701", + "total_credits": 120, + "tags": ["cs"] + }, + "requirements": { + "core": { + "type": "all", + "category": "major", + "courses": ["CS101", "CS201"] + }, + "track": { + "type": "one_of", + "name": "Track", + "options": [ + { + "id": "ai", + "name": "AI Track", + "requirements": [ + {"type": "all", "courses": ["CS300"]}, + {"type": "select", "count": 1, "from": {"courses": ["CS400", "CS401"]}} + ] + } + ] + } + }, + "courses": { + "CS101": {"name": "Intro", "prefix": "CS", "number": "101", "credit_hours": 4.0}, + "CS201": {"name": "Data Structures", "prefix": "CS", "number": "201", "credit_hours": 4.0, "prerequisites": "CS101"}, + "CS300": {"name": "AI", "prefix": "CS", "number": "300", "credit_hours": 3.0}, + "CS400": {"name": "ML", "prefix": "CS", "number": "400", "credit_hours": 3.0}, + "CS401": {"name": "NLP", "prefix": "CS", "number": "401", "credit_hours": 3.0} + }, + "analysis": { + "metrics": { + "complexity": {"mean": 12.5}, + "delay": {"mean": 3.0}, + "credits": {"mean": 120.0} + }, + "sample_type": "shuffled", + "variations_run": 137 + }, + "selected_plans": [ + {"category": "Shortest Path", "course_count": 5, "credits": 120.0, + "critical_path": ["CS101", "CS201"], "is_calc_ready": false, + "longest_delay": 3, "schedule": [{"term": 1, "courses": ["CS101"], "credits": 4.0}], + "terms_required": 8, "total_complexity": 12} + ] + }"# + .to_string() + } + + #[test] + fn test_program_key_unitid_tier() { + let report = sample_report(); + let (plan, _) = build_import_plan(&report, &opts(), Some(167_358), 1).unwrap(); + let key = &plan.program.as_ref().unwrap().program_key; + assert_eq!(key, "prog:167358|11.0701|2024-2025|BS"); + } + + #[test] + fn test_program_key_id_catalog_tier() { + // No unitid resolved, but degree carries an id. + let report = r#"{ + "degree": {"name": "CS", "degree_type": "BS", "system_type": "semester", + "id": "test-cs-2024", "catalog_year": "2024-2025"}, + "requirements": {}, "courses": {} + }"#; + let (plan, _) = build_import_plan(report, &opts(), None, 1).unwrap(); + let key = &plan.program.as_ref().unwrap().program_key; + assert_eq!(key, "prog:test-cs-2024|2024-2025"); + } + + #[test] + fn test_program_key_fingerprint_tier_and_catalog_year_matters() { + // No unitid, no id → fingerprint. Different catalog_year ⇒ different key. + let base = r#"{ + "degree": {"name": "CS", "degree_type": "BS", "system_type": "semester", + "institution": "Test University", "catalog_year": "CATYEAR"}, + "requirements": {}, "courses": {} + }"#; + let r2024 = base.replace("CATYEAR", "2024-2025"); + let r2025 = base.replace("CATYEAR", "2025-2026"); + let (p2024, _) = build_import_plan(&r2024, &opts(), None, 1).unwrap(); + let (p2025, _) = build_import_plan(&r2025, &opts(), None, 1).unwrap(); + let k24 = &p2024.program.as_ref().unwrap().program_key; + let k25 = &p2025.program.as_ref().unwrap().program_key; + assert!( + k24.starts_with("fp:"), + "expected fingerprint key, got {k24}" + ); + assert!(k25.starts_with("fp:")); + assert_ne!(k24, k25, "different catalog_year must yield different keys"); + } + + #[test] + fn test_institution_slug() { + assert_eq!( + institution_slug(" Northeastern University "), + "northeastern-university" + ); + assert_eq!( + institution_slug("Texas A&M (College Station)"), + "texas-a-m-college-station" + ); + assert_eq!(institution_slug("UC—Berkeley!!!"), "uc-berkeley"); + } + + #[test] + fn test_institution_ref_prefers_unitid() { + assert_eq!( + institution_ref(Some(167_358), Some("Test University")), + "167358" + ); + assert_eq!( + institution_ref(None, Some("Test University")), + "test-university" + ); + assert_eq!(institution_ref(None, None), "unknown"); + } + + #[test] + fn test_req_path_for_nested_one_of() { + let report = sample_report(); + let (plan, _) = build_import_plan(&report, &opts(), Some(1), 1).unwrap(); + // Top-level `track` is a one_of; its option `ai` has two nested reqs. + let paths: Vec<&str> = plan + .requirements + .iter() + .map(|r| r.req_path.as_str()) + .collect(); + assert!(paths.contains(&"core")); + assert!(paths.contains(&"track")); + assert!(paths.contains(&"track#ai#0"), "nested req 0: {paths:?}"); + assert!(paths.contains(&"track#ai#1"), "nested req 1: {paths:?}"); + + let nested = plan + .requirements + .iter() + .find(|r| r.req_path == "track#ai#1") + .unwrap(); + assert_eq!(nested.parent_path.as_deref(), Some("track")); + assert_eq!(nested.option_id.as_deref(), Some("ai")); + assert_eq!(nested.option_name.as_deref(), Some("AI Track")); + assert!(nested.map_key.is_none()); + assert_eq!(nested.req_type, "select"); + + // Top-level `core` carries map_key, no parent/option. + let core = plan + .requirements + .iter() + .find(|r| r.req_path == "core") + .unwrap(); + assert_eq!(core.map_key.as_deref(), Some("core")); + assert!(core.parent_path.is_none()); + assert!(core.option_id.is_none()); + } + + #[test] + fn test_allow_double_count_derivation() { + use crate::core::models::degree::{Requirement, RequirementConstraints, RequirementType}; + let mk = |exclude_used: Option| Requirement { + name: None, + req_type: RequirementType::All, + category: None, + courses: None, + from: None, + count: None, + credits: None, + credit_range: None, + constraints: exclude_used.map(|eu| RequirementConstraints { + exclude_used: Some(eu), + distinct_subjects: None, + min_upper_division: None, + max_from_subject: None, + max_from_pattern: None, + max_from_pattern_credits: None, + grade_minimum: None, + }), + options: None, + tags: None, + }; + // exclude_used = true → no double counting. + assert_eq!(allow_double_count(&mk(Some(true)), Some(true)), Some(false)); + // exclude_used = false → double counting allowed. + assert_eq!(allow_double_count(&mk(Some(false)), None), Some(true)); + // unset → falls back to the program default. + assert_eq!(allow_double_count(&mk(None), Some(true)), Some(true)); + assert_eq!(allow_double_count(&mk(None), None), None); + } + + #[test] + fn test_is_impossible_count_exceeds_pool() { + use crate::core::models::degree::{FromClause, Requirement, RequirementType}; + let req = Requirement { + name: None, + req_type: RequirementType::Select, + category: None, + courses: None, + from: Some(FromClause { + courses: Some(vec!["CS1".into(), "CS2".into()]), + pattern: None, + include: None, + exclude: None, + groups: None, + groups_required: None, + per_group: None, + }), + count: Some(3), // 3 > pool of 2 → impossible + credits: None, + credit_range: None, + constraints: None, + options: None, + tags: None, + }; + assert!(requirement_is_impossible(&req)); + } + + #[test] + fn test_build_plan_counts() { + let report = sample_report(); + let (plan, warnings) = build_import_plan(&report, &opts(), Some(1), 7).unwrap(); + assert!(warnings.is_empty(), "unified JSON path emits no warnings"); + assert!(plan.is_full); + // 5 courses → 5 course rows + 5 junction rows. + assert_eq!(plan.courses.len(), 5); + assert_eq!(plan.program_courses.len(), 5); + // Requirements: core + track + 2 nested = 4. + assert_eq!(plan.requirements.len(), 4); + // Analysis present → run + 1 plan. No per-course metrics in this report. + assert!(plan.run.is_some()); + assert_eq!(plan.plans.len(), 1); + assert_eq!(plan.course_metrics.len(), 0); + // Generation propagated. + assert_eq!(plan.program.as_ref().unwrap().generation, 7); + // Degree-level means promoted off analysis.metrics. + let run = plan.run.as_ref().unwrap(); + assert!((run.complexity_mean.unwrap() - 12.5).abs() < f32::EPSILON); + assert!((run.credits_mean.unwrap() - 120.0).abs() < f32::EPSILON); + assert_eq!(run.variations_run, Some(137)); + assert_eq!(run.sample_type.as_deref(), Some("shuffled")); + // Full run: analyzed_document not stored, not trimmed. + assert!(run.analyzed_document.is_none()); + assert!(!run.trimmed); + assert_eq!( + run.analyzed_document_hash, + plan.program.as_ref().unwrap().document_hash + ); + // Structured prereq tree built for CS201. + let cs201 = plan + .courses + .iter() + .find(|c| c.course_code == "CS201") + .unwrap(); + assert!(cs201.prerequisites.is_some()); + } + + #[test] + fn test_course_metrics_built_from_report() { + // A report whose courses carry a `metrics` object → course-metric rows. + let report = r#"{ + "degree": {"name": "CS", "degree_type": "BS", "system_type": "semester", "institution": "U"}, + "requirements": {"core": {"type": "all", "courses": ["CS101"]}}, + "courses": { + "CS101": {"name": "Intro", "prefix": "CS", "number": "101", "credit_hours": 4.0, + "metrics": {"complexity": {"mean": 1.0}, "centrality": {"mean": 0.0}, + "delay": {"mean": 1.0}, "blocking": {"mean": 0.0}, + "course_id": "CS101", "plan_count": 42}} + }, + "analysis": {"metrics": {"complexity": {"mean": 1.0}}, "sample_type": "sequential", "variations_run": 1} + }"#; + let (plan, _) = build_import_plan(report, &opts(), Some(1), 1).unwrap(); + assert_eq!(plan.course_metrics.len(), 1); + let m = &plan.course_metrics[0]; + assert_eq!(m.course_code, "CS101"); + assert_eq!(m.plan_count, Some(42)); + assert!((m.complexity_mean.unwrap() - 1.0).abs() < f32::EPSILON); + // course_id dropped from the stored metrics blob. + assert!(m.metrics.as_ref().unwrap().get("course_id").is_none()); + } + + #[test] + fn test_non_full_variant_sets_analyzed_document_and_trimmed() { + let report = sample_report(); + let mut o = opts(); + o.variant = "trimmed".to_string(); + let (plan, _) = build_import_plan(&report, &o, Some(1), 1).unwrap(); + assert!(!plan.is_full); + let run = plan.run.as_ref().unwrap(); + assert!(run.trimmed, "trimmed variant must set trimmed=true"); + assert!( + run.analyzed_document.is_some(), + "non-full variant must store the analyzed_document" + ); + assert_eq!(run.variant, "trimmed"); + } + + #[test] + fn test_content_hash_is_stable_hex_sha256() { + // Known sha256 of the empty string. + assert_eq!( + content_hash(""), + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ); + // Deterministic and hex-only. + let h = content_hash("hello"); + assert_eq!(h.len(), 64); + assert!(h.chars().all(|c| c.is_ascii_hexdigit())); + } + + #[test] + fn test_no_analysis_block_yields_no_run() { + let report = r#"{ + "degree": {"name": "CS", "degree_type": "BS", "system_type": "semester", "institution": "U"}, + "requirements": {"core": {"type": "all", "courses": ["CS101"]}}, + "courses": {"CS101": {"name": "Intro", "prefix": "CS", "number": "101", "credit_hours": 4.0}} + }"#; + let (plan, _) = build_import_plan(report, &opts(), Some(1), 1).unwrap(); + assert!(plan.run.is_none()); + assert!(plan.course_metrics.is_empty()); + assert!(plan.plans.is_empty()); + } +} diff --git a/src/core/database/mod.rs b/src/core/database/mod.rs index e780ee0..1d5d4f1 100644 --- a/src/core/database/mod.rs +++ b/src/core/database/mod.rs @@ -24,6 +24,8 @@ pub mod auth; pub mod client; pub mod error; +#[cfg(feature = "database")] +pub mod import; pub mod ipeds; pub mod models; pub mod query; @@ -40,6 +42,22 @@ pub mod tables { pub const INSTITUTION_COMPLETION_TOTALS: &str = "institution_completion_totals"; /// CIP code taxonomy lookup pub const CIP_CODES: &str = "cip_codes"; + /// Imported degree programs (normalized; one row per program + lossless `document`) + pub const PROGRAMS: &str = "programs"; + /// Shared per-institution course catalog + pub const COURSES: &str = "courses"; + /// M:N junction linking programs to courses (with per-program overrides) + pub const PROGRAM_COURSES: &str = "program_courses"; + /// Flattened requirement tree per program (addressed by `req_path`) + pub const PROGRAM_REQUIREMENTS: &str = "program_requirements"; + /// Lookup for normalized `degree_type` codes + pub const DEGREE_TYPES: &str = "degree_types"; + /// One row per `degree analyze` run of a program (params + variant + degree-level metrics) + pub const ANALYSIS_RUNS: &str = "analysis_runs"; + /// Per run x course graph metrics (complexity/centrality/delay/blocking) + pub const ANALYSIS_COURSE_METRICS: &str = "analysis_course_metrics"; + /// Per run x selected exemplar plan (shortest/longest/samples) + pub const ANALYSIS_PLANS: &str = "analysis_plans"; } pub use crate::core::config::DatabaseConfig; diff --git a/src/core/database/models.rs b/src/core/database/models.rs index c8f2ae9..477aab1 100644 --- a/src/core/database/models.rs +++ b/src/core/database/models.rs @@ -209,6 +209,309 @@ pub struct DemographicRepresentation { pub representation_ratio: Option, } +// ============================================================================= +// Imported degree program tables (see docs/database/programs-schema.sql). +// `id`/`created_at`/`updated_at` are DB-assigned, so they are skipped when None +// (PostgREST rejects bulk INSERTs that declare a DB-assigned column); every other +// optional stays in the payload as `null` so each batch has a uniform key set. +// JSONB columns are `serde_json::Value`; Postgres `text[]` columns are +// `Option>`. +// ============================================================================= + +/// Row in `programs` — one imported degree program (+ lossless `document`). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StoredProgram { + /// Auto-generated record ID + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + /// Deterministic idempotency key (`on_conflict` target) + pub program_key: String, + /// `Degree.id` slug (may be null; non-unique here) + pub degree_id: Option, + /// Resolved IPEDS unit id (no FK) + pub unitid: Option, + /// Course-catalog partition: `unitid` as text when resolved, else institution slug + pub institution_ref: String, + /// Original `Degree.institution` string + pub institution_raw: Option, + /// CIP code (no FK; LEFT JOIN `cip_codes`) + pub cip_code: Option, + /// Program name + pub name: String, + /// Normalized degree type code (→ `degree_types.code`, no FK) + pub degree_type: Option, + /// Derived: major | minor | concentration | certificate | track | … + pub program_kind: Option, + /// Derived discipline: ai | cs | ds | cy | … + pub discipline: Option, + /// `semester` | `quarter` + pub system_type: String, + /// Raw tag set (GIN-indexed) + pub tags: Option>, + /// Catalog year (part of the program identity) + pub catalog_year: Option, + /// Official catalog URL + pub source_url: Option, + /// Credits required to graduate + pub total_credits: Option, + /// Minimum upper-division credits + pub upper_division_credits: Option, + /// Minimum in-major credits + pub in_major_credits: Option, + /// Minimum overall GPA + pub gpa_minimum: Option, + /// Minimum major GPA + pub gpa_major: Option, + /// Default minimum grade + pub grade_minimum: Option, + /// Subject prefixes counting toward the major + pub major_subjects: Option>, + /// Program-level double-counting default (effective control is per-requirement) + pub allow_double_counting: Option, + /// Authoritative unified-JSON degree document (source of truth) + pub document: serde_json::Value, + /// sha256 of the canonical document (change detection) + pub document_hash: String, + /// Human-confirmed; gates overwrite + pub verified: bool, + /// Whether `unitid` was resolved + pub institution_resolved: bool, + /// Whether any requirement is logically impossible (count > pool) + pub has_impossible_requirements: bool, + /// Re-import commit-marker generation + pub generation: i64, + /// Creation timestamp (ISO 8601) + #[serde(skip_serializing_if = "Option::is_none")] + pub created_at: Option, + /// Update timestamp (ISO 8601) + #[serde(skip_serializing_if = "Option::is_none")] + pub updated_at: Option, +} + +/// Row in `courses` — shared per-institution course catalog. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StoredCourse { + /// Auto-generated record ID + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + /// Catalog partition (unitid-as-text or institution slug) + pub institution_ref: String, + /// Resolved IPEDS unit id (no FK) + pub unitid: Option, + /// Course-map key, e.g. "CMPSC121" + pub course_code: String, + /// Subject prefix + pub prefix: Option, + /// Course number + pub number: Option, + /// Course title + pub name: Option, + /// Credit hours + pub credit_hours: Option, + /// Variable-credit minimum + pub credit_min: Option, + /// Variable-credit maximum + pub credit_max: Option, + /// Prerequisite tree ({and|or|leaf}) + pub prerequisites: Option, + /// Raw prerequisite expression string + pub prerequisites_raw: Option, + /// Gen-ed attribute codes + pub gen_ed_attributes: Option>, + /// Cross-listed course codes + pub cross_listed_as: Option>, + /// Course-level tags + pub tags: Option>, + /// Sync generation stamp + pub generation: i64, + /// Creation timestamp (ISO 8601) + #[serde(skip_serializing_if = "Option::is_none")] + pub created_at: Option, + /// Update timestamp (ISO 8601) + #[serde(skip_serializing_if = "Option::is_none")] + pub updated_at: Option, +} + +/// Row in `program_courses` — M:N junction with per-program overrides. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StoredProgramCourse { + /// Auto-generated record ID + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + /// Parent program natural key + pub program_key: String, + /// Joins to `courses` on (`institution_ref`, `course_code`) + pub institution_ref: String, + /// Course-map key + pub course_code: String, + /// Program-specific credit override when it diverges from the canonical course + pub credit_hours_override: Option, + /// Program-specific course name when it diverges + pub name_as_listed: Option, + /// Sync generation stamp (swept per program) + pub generation: i64, +} + +/// Row in `program_requirements` — the requirement tree flattened by `req_path`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StoredProgramRequirement { + /// Auto-generated record ID + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + /// Parent program natural key + pub program_key: String, + /// Deterministic address of this node in the requirement tree + pub req_path: String, + /// Parent `req_path` (NULL for top-level) — `one_of` adjacency + pub parent_path: Option, + /// Top-level requirements-map key (NULL for nested) + pub map_key: Option, + /// `RequirementOption.id` when under a `one_of` option + pub option_id: Option, + /// `RequirementOption.name` + pub option_name: Option, + /// Human-readable requirement name + pub name: Option, + /// `all` | `select` | `one_of` + pub req_type: String, + /// major | supporting | `gen_ed` | elective + pub category: Option, + /// Number of courses to select + pub count: Option, + /// Credits to select + pub credits: Option, + /// Variable-credit minimum + pub credit_min: Option, + /// Variable-credit maximum + pub credit_max: Option, + /// Requirement-level tags + pub tags: Option>, + /// Course list for `type: all` + pub courses: Option, + /// The `from`-clause (courses/pattern/include/exclude/groups/…) + pub selection_spec: Option, + /// `RequirementConstraints` (named to avoid the SQL `CONSTRAINT` keyword) + pub req_constraints: Option, + /// Computed: count > resolvable pool (queryable, not dropped) + pub is_impossible: bool, + /// Derived from `constraints.exclude_used` (+ program default) + pub allow_double_count: Option, + /// Sync generation stamp (swept per program) + pub generation: i64, +} + +/// Row in `analysis_runs` — one `degree analyze` run of a program. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StoredAnalysisRun { + /// Auto-generated record ID + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + /// Deterministic run key (`on_conflict` target) + pub run_key: String, + /// Parent program natural key (no FK; LEFT JOIN programs) + pub program_key: String, + /// Hash of the exact degree analyzed (the trimmed variant if trimmed) + pub analyzed_document_hash: String, + /// `full` | `trimmed` | other transform label + pub variant: String, + /// Convenience flag (variant trims the degree) + pub trimmed: bool, + /// The analyzed degree when it differs from the program's document; NULL for full runs + pub analyzed_document: Option, + /// Plans enumerated/sampled + pub variations_run: Option, + /// `sequential` | `shuffled` | `stratified` + pub sample_type: Option, + /// `mean` | `median` + pub calc_strategy: Option, + /// Plan enumeration strategy + pub sampling_strategy: Option, + /// Plan generation cap used + pub max_plans: Option, + /// Whether the full plan space was generated + pub full_run: Option, + /// Courses pinned into every plan + pub included_courses: Option>, + /// Degree-level metric stats {complexity, credits, delay} + pub degree_metrics: Option, + /// Promoted mean complexity (for ranking) + pub complexity_mean: Option, + /// Promoted mean delay + pub delay_mean: Option, + /// Promoted mean credits + pub credits_mean: Option, + /// Sync generation stamp + pub generation: i64, + /// Creation timestamp (ISO 8601) + #[serde(skip_serializing_if = "Option::is_none")] + pub created_at: Option, + /// Update timestamp (ISO 8601) + #[serde(skip_serializing_if = "Option::is_none")] + pub updated_at: Option, +} + +/// Row in `analysis_course_metrics` — per run × course graph metrics. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StoredAnalysisCourseMetric { + /// Auto-generated record ID + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + /// Parent run key (`on_conflict` target with `course_code`) + pub run_key: String, + /// Denormalized program key for cross-run course queries + pub program_key: String, + /// Course-map key + pub course_code: String, + /// How many variations included this course + pub plan_count: Option, + /// Promoted mean complexity + pub complexity_mean: Option, + /// Promoted mean centrality + pub centrality_mean: Option, + /// Promoted mean delay + pub delay_mean: Option, + /// Promoted mean blocking + pub blocking_mean: Option, + /// Full 7-stat breakdown {complexity, centrality, delay, blocking} + pub metrics: Option, + /// Sync generation stamp + pub generation: i64, +} + +/// Row in `analysis_plans` — per run × selected exemplar plan. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StoredAnalysisPlan { + /// Auto-generated record ID + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + /// Parent run key (`on_conflict` target with `plan_index`) + pub run_key: String, + /// Denormalized program key + pub program_key: String, + /// Position within the run's `selected_plans` + pub plan_index: i32, + /// `Shortest Path` | `Longest Path` | `Random Sample` | … + pub category: Option, + /// Terms required to complete this plan + pub terms_required: Option, + /// Total curriculum complexity of the plan + pub total_complexity: Option, + /// Longest delay factor in the plan + pub longest_delay: Option, + /// Total credits in the plan + pub credits: Option, + /// Number of courses in the plan + pub course_count: Option, + /// Whether the plan is ready for the calc tool + pub is_calc_ready: Option, + /// Critical path course codes + pub critical_path: Option, + /// Term-by-term schedule [{term, courses, credits}, …] + pub schedule: Option, + /// Sync generation stamp + pub generation: i64, +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/core/models/degree.rs b/src/core/models/degree.rs index 9a628d6..3ee08cc 100644 --- a/src/core/models/degree.rs +++ b/src/core/models/degree.rs @@ -38,6 +38,10 @@ pub struct Degree { #[serde(skip_serializing_if = "Option::is_none")] pub institution: Option, + /// IPEDS unit id, when known + #[serde(default, skip_serializing_if = "Option::is_none")] + pub unitid: Option, + /// Catalog year (e.g., "2024-2025") #[serde(skip_serializing_if = "Option::is_none")] pub catalog_year: Option, @@ -276,6 +280,7 @@ impl Degree { // Extended fields default to None id: None, institution: None, + unitid: None, catalog_year: None, source_url: None, total_credits: None, @@ -312,6 +317,7 @@ impl Degree { system_type, id: Some(id), institution: Some(institution), + unitid: None, catalog_year: Some(catalog_year), source_url: None, total_credits: Some(total_credits), diff --git a/src/mcp/server.rs b/src/mcp/server.rs index b698b76..870c9cf 100644 --- a/src/mcp/server.rs +++ b/src/mcp/server.rs @@ -25,10 +25,10 @@ use rmcp::{ use crate::core::database::DbClient; #[cfg(feature = "database")] use crate::mcp::tools::{ - cip_codes, completions, degrees, institutions, lookup, scaffold, CompareDegreesRequest, + cip_codes, completions, degrees, import, institutions, lookup, scaffold, CompareDegreesRequest, CompletionDemographicsRequest, GetDegreeRequest, GetInstitutionCompletionsRequest, GetInstitutionRequest, GetLookupCodesRequest, GetSchoolsCompletionDemographicsRequest, - ScaffoldDegreeYamlRequest, SearchCipCodesRequest, SearchDegreesRequest, + ImportDegreeRequest, ScaffoldDegreeYamlRequest, SearchCipCodesRequest, SearchDegreesRequest, SearchInstitutionsRequest, }; // `StoreDegreeRequest` import kept out of the active list while the @@ -538,6 +538,17 @@ impl NuAnalyticsMcpServer { }) } + /// Import a degree report into the normalized program tables + #[cfg(feature = "database")] + #[tool( + description = "Import a degree-first analysis report (or a plain unified degree) JSON into the normalized program tables (programs, courses, program_courses, program_requirements); when the input carries an `analysis` block, one analysis run is also attached. Provide exactly ONE source: json_content (inline) or json_path (file on the server's filesystem). Set dry_run=true to preview the write counts without touching the database. The institution is resolved read-only; when the name matches multiple institutions the result is \"institution_ambiguous\" with institution_candidates [{unitid, name}] — re-call with an explicit unitid to pick one. variant defaults to \"full\" (writes the program projection); a non-full variant only attaches an analysis run. Use force/replace to overwrite an existing program and skip_existing to leave it untouched." + )] + fn import_degree(&self, Parameters(req): Parameters) -> String { + self.call_db("import_degree", |db| async move { + import::execute_json(&db, req).await + }) + } + /// Run a degree-pipeline tool against a [`YamlSource`]. /// /// Resolves the YAML from inline content, a filesystem path, or a stored diff --git a/src/mcp/tools/import.rs b/src/mcp/tools/import.rs new file mode 100644 index 0000000..44e9623 --- /dev/null +++ b/src/mcp/tools/import.rs @@ -0,0 +1,371 @@ +//! `import_degree` MCP tool. +//! +//! Wraps the tested import core ([`crate::core::database::import::execute_import`]) +//! so an AI client can load a degree-first analysis report (or a plain unified +//! degree) JSON into the normalized program tables (`programs`, `courses`, +//! `program_courses`, `program_requirements`) plus, when the input carries an +//! `analysis` block, one analysis run. +//! +//! The tool resolves the report text (inline `json_content` or a `json_path` +//! file on the server's filesystem), builds [`ImportOptions`], calls +//! `execute_import`, and maps the [`ImportOutcome`] into a structured JSON +//! response. `dry_run` previews the write counts without touching the DB; an +//! ambiguous institution name returns the candidate `(unitid, name)` pairs so +//! the agent can re-call with an explicit `unitid`. + +use std::sync::Arc; + +use crate::core::database::import::{execute_import, ImportOptions, ImportOutcome, ImportResult}; +use crate::core::database::DbClient; +use crate::mcp::tools::shared::{self, error_json}; +use rmcp::schemars; +use serde::Deserialize; + +// ============================================================================ +// Request type +// ============================================================================ + +/// Request parameters for the `import_degree` tool. +/// +/// Provide exactly one report source: `json_content` (inline string) or +/// `json_path` (a file the MCP server reads from its filesystem). The remaining +/// fields mirror the import core's [`ImportOptions`] overrides. +#[derive(Debug, Deserialize, schemars::JsonSchema)] +pub struct ImportDegreeRequest { + /// Inline degree-first report (or unified degree) JSON. Mutually exclusive + /// with `json_path`; exactly one is required. + #[schemars( + description = "Inline degree-first report (or unified degree) JSON. Mutually exclusive with json_path; provide exactly one." + )] + pub json_content: Option, + + /// Filesystem path to a report JSON file the server will read. Mutually + /// exclusive with `json_content`; exactly one is required. + #[schemars( + description = "Path to a report JSON file on the MCP server's filesystem. Mutually exclusive with json_content; provide exactly one." + )] + pub json_path: Option, + + /// Analysis-run variant label (default `"full"`). `full` writes the program + /// projection; a non-full variant only attaches an analysis run. + #[schemars( + description = "Analysis-run variant label (default \"full\"). \"full\" writes the program projection; a non-full variant only attaches an analysis run." + )] + pub variant: Option, + + /// Override the resolved institution IPEDS unit id. Set this to + /// disambiguate when a previous call returned `institution_ambiguous`. + #[schemars( + description = "Override the resolved IPEDS UNITID. Set this to disambiguate after an institution_ambiguous result." + )] + #[serde(default, deserialize_with = "shared::deserialize_opt_i32")] + pub unitid: Option, + + /// Override the institution name used for resolution / display. + #[schemars(description = "Override the institution name used for resolution/display.")] + pub institution: Option, + + /// Override the CIP code (part of the natural program key). + #[schemars(description = "Override the CIP code (part of the natural program key).")] + pub cip: Option, + + /// Override the catalog year (part of the program identity). + #[schemars(description = "Override the catalog year (part of the program identity).")] + pub catalog: Option, + + /// Override the degree id. + #[schemars(description = "Override the degree id.")] + pub degree_id: Option, + + /// Overwrite a verified program / skip the confirmation gate. + #[schemars(description = "Overwrite a verified program / skip the confirmation gate.")] + #[serde(default, deserialize_with = "shared::deserialize_opt_bool")] + pub force: Option, + + /// Replace an existing (unverified) program. + #[schemars(description = "Replace an existing (unverified) program.")] + #[serde(default, deserialize_with = "shared::deserialize_opt_bool")] + pub replace: Option, + + /// Skip the program entirely if it already exists. + #[schemars(description = "Skip the program entirely if it already exists.")] + #[serde(default, deserialize_with = "shared::deserialize_opt_bool")] + pub skip_existing: Option, + + /// Build the plan and report counts but write nothing. + #[schemars(description = "Preview the import (report counts) without writing anything.")] + #[serde(default, deserialize_with = "shared::deserialize_opt_bool")] + pub dry_run: Option, +} + +// ============================================================================ +// Execute +// ============================================================================ + +/// Execute `import_degree` and return JSON. +/// +/// Resolves the report text from `json_content` / `json_path` (exactly one +/// required), builds [`ImportOptions`], runs the import core, and maps the +/// [`ImportOutcome`] to a structured JSON string. Transport/parse failures +/// surface via [`error_json`]. +pub async fn execute_json(client: &Arc, req: ImportDegreeRequest) -> String { + let text = match resolve_report_text(req.json_content, req.json_path) { + Ok(t) => t, + Err(e) => return e, + }; + + let opts = ImportOptions { + variant: req.variant.unwrap_or_else(|| "full".to_string()), + unitid: req.unitid, + institution: req.institution, + cip_code: req.cip, + catalog_year: req.catalog, + degree_id: req.degree_id, + force: req.force.unwrap_or(false), + replace: req.replace.unwrap_or(false), + skip_existing: req.skip_existing.unwrap_or(false), + dry_run: req.dry_run.unwrap_or(false), + }; + + match execute_import(client, &text, &opts).await { + Ok(outcome) => outcome_to_json(&outcome).to_string(), + Err(e) => error_json(e), + } +} + +/// Resolve the report text from exactly one of `json_content` / `json_path`. +/// +/// Returns a JSON error string (ready to surface) when neither or both are +/// provided, or when a `json_path` file cannot be read. +fn resolve_report_text( + json_content: Option, + json_path: Option, +) -> Result { + match (json_content, json_path) { + (Some(_), Some(_)) => Err(error_json( + "Provide exactly one of: json_content or json_path (not both)", + )), + (None, None) => Err(error_json( + "Must provide exactly one of: json_content or json_path", + )), + (Some(c), None) => Ok(c), + (None, Some(p)) => shared::read_yaml_file(&p), + } +} + +/// Map an [`ImportOutcome`] to the structured JSON response. +/// +/// `ImportOutcome` is not `Serialize`, so the object is built explicitly with +/// `serde_json::json!`. The `result` field carries a stable lowercase tag; the +/// `NeedsConfirmation` / `InstitutionAmbiguous` / `Rejected` variants attach +/// their extra payload (`reason` / `institution_candidates` / `errors`). +fn outcome_to_json(outcome: &ImportOutcome) -> serde_json::Value { + let mut value = serde_json::json!({ + "result": result_tag(&outcome.result), + "program_key": outcome.program_key, + "resolved_unitid": outcome.resolved_unitid, + "institution": outcome.institution, + "variant": outcome.variant, + "variations_run": outcome.variations_run, + "sample_type": outcome.sample_type, + "courses_written": outcome.courses_written, + "requirements_written": outcome.requirements_written, + "run_written": outcome.run_written, + "plans_written": outcome.plans_written, + "course_metrics_written": outcome.course_metrics_written, + "conversion_warnings": outcome.conversion_warnings, + "messages": outcome.messages, + }); + + // `value` is built from a json! object literal, so it is always an object. + if let Some(map) = value.as_object_mut() { + match &outcome.result { + ImportResult::NeedsConfirmation(reason) => { + map.insert("reason".to_string(), serde_json::json!(reason)); + } + ImportResult::InstitutionAmbiguous(candidates) => { + let cands: Vec = candidates + .iter() + .map(|(unitid, name)| serde_json::json!({ "unitid": unitid, "name": name })) + .collect(); + map.insert( + "institution_candidates".to_string(), + serde_json::Value::Array(cands), + ); + } + ImportResult::Rejected(errors) => { + map.insert("errors".to_string(), serde_json::json!(errors)); + } + ImportResult::Created | ImportResult::Updated | ImportResult::Skipped => {} + } + } + + value +} + +/// Stable lowercase tag for an [`ImportResult`] variant. +const fn result_tag(result: &ImportResult) -> &'static str { + match result { + ImportResult::Created => "created", + ImportResult::Updated => "updated", + ImportResult::Skipped => "skipped", + ImportResult::NeedsConfirmation(_) => "needs_confirmation", + ImportResult::InstitutionAmbiguous(_) => "institution_ambiguous", + ImportResult::Rejected(_) => "rejected", + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Build a default `ImportOutcome` with the given result for response-shape + /// tests. Avoids calling `execute_import`, which needs a live DB. + fn outcome_with(result: ImportResult) -> ImportOutcome { + ImportOutcome { + program_key: "prog:167358|11.0701|2024-2025|BS".to_string(), + result, + resolved_unitid: Some(167_358), + variant: "full".to_string(), + institution: Some("Test University".to_string()), + variations_run: Some(137), + sample_type: Some("shuffled".to_string()), + courses_written: 5, + requirements_written: 4, + run_written: true, + plans_written: 1, + course_metrics_written: 0, + conversion_warnings: vec![], + messages: vec!["ok".to_string()], + } + } + + #[test] + fn request_deserializes_with_lenient_scalars() { + // Mirror the lenient deserializers used elsewhere: stringified unitid + // and string booleans must coerce cleanly. + let req: ImportDegreeRequest = serde_json::from_value(serde_json::json!({ + "json_content": "{}", + "unitid": "167358", + "dry_run": "true", + "force": false, + })) + .expect("request must deserialize"); + assert_eq!(req.unitid, Some(167_358)); + assert_eq!(req.dry_run, Some(true)); + assert_eq!(req.force, Some(false)); + assert_eq!(req.json_content.as_deref(), Some("{}")); + } + + #[test] + fn request_defaults_optional_fields_to_none() { + let req: ImportDegreeRequest = + serde_json::from_value(serde_json::json!({ "json_path": "/tmp/r.json" })) + .expect("request must deserialize"); + assert_eq!(req.json_path.as_deref(), Some("/tmp/r.json")); + assert!(req.json_content.is_none()); + assert!(req.variant.is_none()); + assert!(req.unitid.is_none()); + assert!(req.dry_run.is_none()); + } + + #[test] + fn resolve_report_text_requires_exactly_one_source() { + assert!(resolve_report_text(None, None) + .unwrap_err() + .contains("Must provide exactly one")); + assert!( + resolve_report_text(Some("{}".to_string()), Some("/tmp/r.json".to_string())) + .unwrap_err() + .contains("not both") + ); + assert_eq!( + resolve_report_text(Some("{\"a\":1}".to_string()), None).unwrap(), + "{\"a\":1}" + ); + } + + #[test] + fn updated_outcome_carries_counts_no_extra_payload() { + let value = outcome_to_json(&outcome_with(ImportResult::Updated)); + assert_eq!(value["result"], "updated"); + assert_eq!(value["courses_written"], 5); + assert!(value.get("reason").is_none()); + assert!(value.get("institution_candidates").is_none()); + assert!(value.get("errors").is_none()); + } + + #[test] + fn skipped_outcome_carries_counts_no_extra_payload() { + let value = outcome_to_json(&outcome_with(ImportResult::Skipped)); + assert_eq!(value["result"], "skipped"); + assert_eq!(value["courses_written"], 5); + assert!(value.get("reason").is_none()); + assert!(value.get("institution_candidates").is_none()); + assert!(value.get("errors").is_none()); + } + + #[test] + fn ambiguous_outcome_maps_to_institution_candidates() { + let outcome = outcome_with(ImportResult::InstitutionAmbiguous(vec![ + (167_358, "Northeastern University".to_string()), + (166_027, "Harvard University".to_string()), + ])); + let value = outcome_to_json(&outcome); + assert_eq!(value["result"], "institution_ambiguous"); + let cands = value["institution_candidates"] + .as_array() + .expect("candidates array"); + assert_eq!(cands.len(), 2); + assert_eq!(cands[0]["unitid"], 167_358); + assert_eq!(cands[0]["name"], "Northeastern University"); + assert_eq!(cands[1]["unitid"], 166_027); + // No reason/errors keys on this variant. + assert!(value.get("reason").is_none()); + assert!(value.get("errors").is_none()); + } + + #[test] + fn needs_confirmation_outcome_carries_reason() { + let outcome = outcome_with(ImportResult::NeedsConfirmation( + "verified program; pass --force to overwrite".to_string(), + )); + let value = outcome_to_json(&outcome); + assert_eq!(value["result"], "needs_confirmation"); + assert_eq!( + value["reason"], + "verified program; pass --force to overwrite" + ); + assert!(value.get("institution_candidates").is_none()); + } + + #[test] + fn rejected_outcome_carries_errors() { + let outcome = outcome_with(ImportResult::Rejected(vec![ + "bad thing".to_string(), + "other bad thing".to_string(), + ])); + let value = outcome_to_json(&outcome); + assert_eq!(value["result"], "rejected"); + let errors = value["errors"].as_array().expect("errors array"); + assert_eq!(errors.len(), 2); + assert_eq!(errors[0], "bad thing"); + } + + #[test] + fn created_outcome_carries_counts_and_no_extra_payload() { + let outcome = outcome_with(ImportResult::Created); + let value = outcome_to_json(&outcome); + assert_eq!(value["result"], "created"); + assert_eq!(value["program_key"], "prog:167358|11.0701|2024-2025|BS"); + assert_eq!(value["resolved_unitid"], 167_358); + assert_eq!(value["courses_written"], 5); + assert_eq!(value["requirements_written"], 4); + assert_eq!(value["run_written"], true); + assert_eq!(value["plans_written"], 1); + assert_eq!(value["variations_run"], 137); + assert!(value.get("reason").is_none()); + assert!(value.get("institution_candidates").is_none()); + assert!(value.get("errors").is_none()); + } +} diff --git a/src/mcp/tools/mod.rs b/src/mcp/tools/mod.rs index 3235c67..5e1a610 100644 --- a/src/mcp/tools/mod.rs +++ b/src/mcp/tools/mod.rs @@ -27,6 +27,8 @@ pub mod completions; #[cfg(feature = "database")] pub mod degrees; #[cfg(feature = "database")] +pub mod import; +#[cfg(feature = "database")] pub mod institutions; #[cfg(feature = "database")] pub mod lookup; @@ -66,6 +68,8 @@ pub use degrees::{ CompareDegreesRequest, GetDegreeRequest, SearchDegreesRequest, StoreDegreeRequest, }; #[cfg(feature = "database")] +pub use import::ImportDegreeRequest; +#[cfg(feature = "database")] pub use institutions::{GetInstitutionRequest, SearchInstitutionsRequest}; #[cfg(feature = "database")] pub use lookup::GetLookupCodesRequest;