{"id":50763555,"url":"https://github.com/pholser/opt-dog","last_synced_at":"2026-06-11T12:30:49.338Z","repository":{"id":353875633,"uuid":"392526518","full_name":"pholser/opt-dog","owner":"pholser","description":null,"archived":false,"fork":false,"pushed_at":"2026-04-26T00:50:40.000Z","size":4134,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-04-26T02:24:59.288Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pholser.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2021-08-04T02:52:07.000Z","updated_at":"2026-04-26T00:50:45.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/pholser/opt-dog","commit_stats":null,"previous_names":["pholser/opt-dog"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/pholser/opt-dog","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pholser%2Fopt-dog","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pholser%2Fopt-dog/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pholser%2Fopt-dog/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pholser%2Fopt-dog/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pholser","download_url":"https://codeload.github.com/pholser/opt-dog/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pholser%2Fopt-dog/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34199516,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-11T02:00:06.485Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-11T12:30:48.431Z","updated_at":"2026-06-11T12:30:49.329Z","avatar_url":"https://github.com/pholser.png","language":"HTML","funding_links":[],"categories":[],"sub_categories":[],"readme":"# AKC All-Breed Show Scheduling\n\nAutomated scheduling system for AKC all-breed conformation dog shows. Reads a structured\nshow input workbook, applies AKC rules and constraints, and produces an optimized judging\nprogram whose primary goal is to minimize the Best-in-Show start time.\n\n---\n\n## Table of Contents\n\n1. [Problem Description](#1-problem-description)\n2. [AKC Rules and Constraints](#2-akc-rules-and-constraints)\n3. [Solution Approach](#3-solution-approach)\n4. [Preprocessing](#4-preprocessing)\n5. [Mathematical Model](#5-mathematical-model)\n6. [Objective](#6-objective)\n7. [Post-hoc Arena Assignment](#7-post-hoc-arena-assignment)\n8. [Output](#8-output)\n9. [Files](#9-files)\n10. [Running the Solver](#10-running-the-solver)\n11. [Known Limitations and Deferred Features](#11-known-limitations-and-deferred-features)\n\n---\n\n## 1. Problem Description\n\nAn AKC all-breed conformation dog show is a single-day event in which purebred dogs compete\nfor the title of Best in Show (BIS). Competition proceeds in three stages:\n\n**Breed judging.** Within each breed, dogs compete by sex and class (class dogs → class\nbitches → specials, i.e., champions). The breed judge awards Winners Dog, Winners Bitch,\nand Best of Breed (BOB).\n\n**Group judging.** The seven BOB winners from each AKC group (Sporting, Hound, Working,\nTerrier, Toy, Non-Sporting, Herding) compete before a group judge. Group judging cannot\nbegin until all breed judging within that group is complete.\n\n**Best in Show.** The seven group winners compete before the BIS judge. BIS cannot begin\nuntil all group judging is complete.\n\nA typical large all-breed show involves:\n- 175–200 AKC breeds and varieties\n- 1,500–4,000 individual dog entries\n- 8–15 judging rings operating simultaneously\n- 12–30 judges, each assigned to one or more breeds plus optionally a group\n- 7–9 hours of judging\n\nThe show committee fixes which judge judges which breeds before the optimizer runs. The\noptimizer decides:\n\n1. Which physical ring each judge's *segment* (block of breeds) occupies, and\n2. When each segment starts.\n\nThe official output — the **judging program** — specifies, for every ring, the sequence of\nbreeds and their approximate start times. Today this program is produced manually by\nexperienced show superintendents. Automation produces optimal or near-optimal schedules in\nminutes and responds instantly to entry changes.\n\n---\n\n## 2. AKC Rules and Constraints\n\n### 2.1 Judging Rates\n\n| Judge type | Rate | Source |\n|---|---|---|\n| Standard | 2.4 min/dog (25 dogs/hr) | AKC Show Manual §6 |\n| Permit    | 3.0 min/dog (20 dogs/hr) | AKC Show Manual §6 |\n\nPer-judge override supported via the `Judges` worksheet `rate_mpd` column.\n\n### 2.2 Segment Size (AKC Show Manual §6)\n\nA judge's breed assignment is divided into *segments* — contiguous blocks judged in a single\nring without interruption.\n\n- **Soft cap:** ~1 hour of judging (25 dogs for standard, 20 for permit judges).\n- **Hard cap:** 50 dogs maximum for any multi-breed segment.\n- **Single-breed exception:** a breed with \u003e 30 entries gets its own segment; no cap applies.\n- **Equipment break:** open a new segment when equipment type changes and the soft cap is\n  already met.\n\n### 2.3 Mandatory Lunch Break (Hard Constraint)\n\nJudges whose total breed judging time ≥ 300 minutes must receive a break of at least\n45 minutes within the designated lunch window (typically 11:30 AM – 1:30 PM). The break must\noccur between segments — it cannot interrupt a breed.\n\n### 2.4 Sequencing (Hard Constraints)\n\n- No judge may judge two events simultaneously.\n- No ring may host two events simultaneously.\n- Group judging for group *g* cannot begin until all breeds in *g* have completed BOB.\n- BIS cannot begin until all seven groups have completed.\n\n### 2.5 Judge Conflict Rules (Hard Constraints)\n\n- A judge may not be assigned to both a breed and that breed's group.\n- A judge may not be assigned to both a group and BIS.\n\n### 2.6 Soft Lunch Availability\n\nJudges not requiring a mandatory break should have at least one inter-segment gap during the\nlunch window where they could reasonably eat. Modeled as a soft penalty.\n\n---\n\n## 3. Solution Approach\n\nThe solver backend is **OR-Tools CP-SAT** (`akc_cpsat.py`). All disjunctive scheduling\nconstraints are encoded using `AddNoOverlap` on interval variables — no big-M coefficients\nare needed. This produces a compact, tightly-propagated model that typically finds a\nnear-optimal schedule within a few minutes for medium-sized shows.\n\n### 3.1 Key Design Decisions\n\n**No pre-designated arena rings.** All rings are available for breed segments. After solving,\n`assign_arena_ring()` selects the ring whose last breed segment ends earliest as the arena\nring and sequences group and BIS events there sequentially. This eliminates the \"vacate\narena\" constraint from the optimizer and lets the objective naturally incentivise early\ncompletion of breed judging.\n\n**Segment-level scheduling.** The optimizer assigns *segments* (not individual breeds) to\nrings and time slots. Breed order within a segment is fixed by preprocessing. This\nsignificantly reduces problem size.\n\n**Time-slot discretization.** Time is discretized into 5-minute slots. All durations and\nstart times are integer multiples of this slot. CP-SAT IntVar domains are bounded tightly\nusing critical-path lower bounds.\n\n**Horizon.** `T = max_judge_load + Σ Dg[g] + D_BIS`. The `tau_bis` IntVar is lower-bounded\nby `max(critical_path_lb, ceil(total_seg_slots / n_rings))`.\n\n---\n\n## 4. Preprocessing (`akc_preprocessing.py`)\n\n### 4.1 Segment Packing\n\nBreeds for each judge are sorted by `(equipment_order, n_total desc)` to cluster equipment\ntypes and push large breeds to the front. They are then greedily packed into segments\nfollowing the size rules in §2.2. Equipment breaks open a new segment when equipment type\nchanges and the soft cap is already met.\n\n**Resulting segment fields:**\n\n| Field | Type | Description |\n|---|---|---|\n| `segment_id` | str | `SEG0001`, `SEG0002`, … |\n| `judge_id` | str | owning judge |\n| `breed_ids` | list[str] | breeds in judging order |\n| `equipment_sequence` | list[str] | equipment per breed (table/ramp/floor) |\n| `duration_slots` | int | `ceil(n_dogs * rate_mpd / slot_min)` |\n| `n_dogs` | int | total entries |\n| `has_equipment_mix` | bool | multiple equipment types in segment |\n\n### 4.2 Time Model\n\n- Slot duration: configurable (default 5 min).\n- `T0` = absolute slot index when judging begins (e.g., 8:00 AM).\n- `T` = total slots in the CP-SAT horizon (relative, 0-indexed).\n- All model variables are relative to `T0`; add `T0` to convert to absolute slot.\n\n### 4.3 Lunch Classification\n\n- **Mandatory-break judges** (`judges_requiring_lunch`): total breed judging time ≥ 300 min.\n  Get a hard lunch gap constraint (C12).\n- **Soft-lunch judges**: all other multi-segment judges. Incur a penalty if no inter-segment\n  gap falls in the lunch window (C13).\n\n---\n\n## 5. Mathematical Model (`akc_cpsat.py`)\n\n### 5.1 Index Sets\n\n| Symbol | Definition |\n|---|---|\n| S | all segment IDs |\n| R | all ring IDs (all rings available to breed segments) |\n| J | all judges |\n| G | all AKC groups (Sporting, Hound, Working, Terrier, Toy, Non-Sporting, Herding) |\n| SP | same-judge segment pairs `{(sa,sb) : sa \u003c sb, same judge}` |\n| XP | cross-judge segment pairs `S×S \\ SP` |\n| LG | mandatory-lunch gap pairs `{(j,i) : j ∈ J_break, 0 ≤ i \u003c |segs(j)|-1}` |\n| SG | soft-lunch gap pairs (same structure, `j ∈ J_soft`) |\n| JG | judge-group pairs `{(j,g) : judge j judges group g}` |\n| JG_SEG | `{(j,g,s) : (j,g) ∈ JG, s ∈ segs(j)}` |\n| RS | ring-switch pairs `{(j,i) : judge j has ≥2 segments, 0≤i\u003c|segs(j)|-1}` |\n\n### 5.2 Parameters\n\n| Symbol | Type | Description |\n|---|---|---|\n| `D[s]` | int | duration in slots of segment s |\n| `Dg[g]` | int | duration in slots of group g judging |\n| `D_BIS` | int | duration in slots of BIS (fixed, 20 min) |\n| `T` | int | CP-SAT horizon |\n| `T0` | int | absolute slot of judging start |\n| `t_ls`, `t_le` | int | lunch window start/end (relative) |\n| `L_slots` | int | mandatory break duration in slots |\n| `lb[s]` | int | serial lower bound: sum of durations of all earlier segments of same judge |\n\n### 5.3 Decision Variables\n\n| Variable | CP-SAT implementation | Description |\n|---|---|---|\n| `u[s,r]` | `pres[sid][rid]` BoolVar | 1 if segment s in ring r |\n| `start_s[s]` | `start[sid]` IntVar in `[lb[s], T−D[s]]` | start slot of segment s |\n| `tau_g[g]` | `tau_g[gid]` IntVar | start slot of group g |\n| `tau_bis` | `tau_bis` IntVar in `[bis_lb, T−D_BIS]` | BIS start slot |\n| `ell[j]` | `lunch_start_var[jid]` IntVar in `[t_ls, t_le−L]` | mandatory lunch start |\n| `sl_gap[j,i]` | `sl_active[jid][i]` BoolVar | soft-lunch gap qualifies |\n| `lunch_pen[j]` | `lunch_pen[jid]` BoolVar | no qualifying soft-lunch gap |\n| `z[j,i]` | `sw_vars[jid,i]` BoolVar | judge j switches rings at gap i |\n\nOrdering variables (`ord`, `ord_rp`, `ord_arena`, `ord_jg`, `ord_bis`, `lam`) are\n**eliminated** — replaced by `AddNoOverlap` on interval variables, which achieves equivalent\nor tighter propagation without explicit binary variables.\n\n### 5.4 Constraints\n\n#### C1 — Ring assignment (one ring per segment)\n```\nΣ_r u[s,r] = 1    ∀s ∈ S\n```\n*CP-SAT: `AddExactlyOne(pres[sid].values())`*\n\n#### C2 — Segment fits within judging window\nEncoded as the upper bound on `start_s[s]`: `start_s[s] ≤ T − D[s]`.\n\n#### C4 — Ring non-overlap\nFor each ring r, optional interval variables `(start[s], D[s], pres[s][r])` are collected and\npassed to `AddNoOverlap`. Fires only when both segments are present in the same ring.\n\n#### C5 — Judge sequencing\nFor each judge j, a fixed interval is added to the judge's `AddNoOverlap` timeline for each\nsegment. No two segments of the same judge can overlap.\n\n#### C6 — Arena serialization (group events)\nGroup interval variables are added to a single arena `AddNoOverlap` timeline. Groups are\nsequenced without overlap; the arena ring is chosen post-hoc.\n\n#### C8 — Group waits for all BOBs\n```\nstart_s[s] + D[s] ≤ tau_g[g]    ∀g, ∀s containing a breed in g\n```\nDirect precedence constraint; no big-M needed.\n\n#### C9 — BIS waits for all groups\n```\ntau_g[g] + Dg[g] ≤ tau_bis    ∀g ∈ G\n```\n\n#### C10 — All events finish by end of day\nEncoded as upper bounds on `tau_g[g]` and `tau_bis`.\n\n#### C11 — Group/BIS judges don't overlap their breed segments\nGroup and BIS interval variables are added directly to the owning judge's `AddNoOverlap`\ntimeline, preventing breed and group/BIS segments from overlapping.\n\n#### C12 — Mandatory lunch break (hard)\nA fixed-duration lunch interval `(ell[j], L_slots)` is added to judge j's `AddNoOverlap`\ntimeline. `ell[j]` is constrained to `[t_ls, t_le − L_slots]`. The `AddNoOverlap` constraint\nimplicitly selects which inter-segment gap hosts the break.\n\n#### C13 — Soft lunch availability (penalty)\nFor each soft-lunch judge j and gap i:\n\n- **C13a:** gap qualifies only if segment i ends ≤ `t_le − L_slots`\n  *(enforced via `OnlyEnforceIf`)*\n- **C13b:** gap qualifies only if segment i+1 starts ≥ `t_ls`\n  *(enforced via `OnlyEnforceIf`)*\n- **C13c:** `lunch_pen[j] ≥ 1 − Σ_i sl_gap[j,i]`\n  *(via `AddBoolOr`)*\n\n#### C15 — Ring-switch indicator\nFor each consecutive segment pair `(i, i+1)` of judge j and each ring r, a `same_r` BoolVar\nis constrained to be 1 only when both segments are in ring r:\n\n```\nsw[j,i] + Σ_r same_r[j,i,r] = 1\n```\n\n`sw[j,i] = 1` iff the two segments are in different rings.\n\n\n#### C16 — Symmetry breaking\nOmitted — CP-SAT handles ring symmetry natively through its search.\n\n---\n\n## 6. Objective\n\nTwo-level weighted-epsilon hierarchy (single weighted sum):\n\n```\nminimize  w_L1 · tau_bis  +  w_L3 · (Σ_{(j,i)∈RS} z[j,i]  +  Σ_j lunch_pen[j])\n```\n\nWhere:\n- `w_L3 = 1`\n- `L3_max = |RS| + |J_soft| + 1`\n- `w_L1 = L3_max + 1` — any 1-slot BIS improvement beats any L3 gain\n\n**Rationale:** Finishing early (`tau_bis` small) is the primary goal. The secondary goal is\nminimizing ring switches and soft lunch penalties.\n\n---\n\n## 7. Post-hoc Arena Assignment (`assign_arena_ring` in `akc_schedule.py`)\n\nAfter solving, the arena ring is selected and group/BIS events are placed:\n\n1. For each ring, find the slot when its last breed segment ends (`ring_last[r]`).\n2. Choose `arena_ring = argmin_r ring_last[r]` (ring that frees up earliest).\n3. Sequence group events in `arena_ring`, each starting at\n   `max(tau_g[g], current_free_slot)`, in temporal order.\n4. Place BIS immediately after the last group.\n\nThis eliminates the \"vacate arena\" constraint from the optimizer: the objective already\nincentivises finishing breed judging early, and post-hoc assignment picks the ring that is\nalready free.\n\n---\n\n## 8. Output\n\n### 8.1 Judging Program (`akc_program.py`)\n\nGenerates a text judging program listing, for each ring:\n\n- Judge name and assignment\n- Ordered breeds with approximate start times\n- Lunch break annotations (mandatory-break judges: fixed time; soft-break judges: \"Lunch at\n  their discretion\" printed between qualifying inter-segment gaps)\n\n### 8.2 Schedule Visualization (`akc_viz.py`)\n\nGenerates an interactive Plotly HTML Gantt chart showing all segments, group events, and BIS\non a shared timeline.\n\n---\n\n## 9. Files\n\n| File | Role |\n|---|---|\n| `akc_preprocessing.py` | Data loading, segment packing, `ShowData` dataclass |\n| `akc_schedule.py` | Shared types: `SolveParams`, `SolveResult`, `SegmentSchedule`, `GroupSchedule`, `assign_arena_ring` |\n| `akc_cpsat.py` | OR-Tools CP-SAT solver; `solve_show(show, params) -\u003e SolveResult` |\n| `akc_viz.py` | Interactive Plotly HTML schedule chart from `SolveResult` |\n| `akc_program.py` | Text judging program generator from `SolveResult` |\n| `akc_show_generator.py` | Synthetic show workbook generator for testing |\n| `akc_cpsat_bench.py` | Benchmarking harness for CP-SAT solver |\n| `CLAUDE.md` | Mathematical specification for Claude Code (keep in sync with source files) |\n\n---\n\n## 10. Running the Solver\n\n```bash\n# Solve and print the judging program\npython3 akc_program.py small.xlsx\n\n# Solve with a time limit and visualize\npython3 akc_viz.py medium.xlsx --time-limit 120\n\n# Benchmark constraint families\npython3 akc_cpsat_bench.py small.xlsx\n```\n\n### Solver Parameters (`SolveParams`)\n\n| Parameter | Default | Description |\n|---|---|---|\n| `solver` | `\"cpsat\"` | Solver backend |\n| `time_limit_sec` | `300` | Wall-clock time limit |\n| `gap` | `0.01` | Optimality gap tolerance (1%) |\n| `threads` | `0` | Worker threads (0 = auto) |\n| `tee` | `True` | Print solver progress |\n\n---\n\n## 11. Known Limitations and Deferred Features\n\n- **Warm start / solution hints:** CP-SAT `AddHint()` greedy construction not yet implemented.\n  The solver starts from scratch and relies on its own heuristics for the first incumbent.\n- **Exhibitor conflict avoidance:** handlers showing multiple dogs in overlapping breeds are\n  not yet modeled. Planned as a future soft-penalty term.\n- **Search strategy:** `AddDecisionStrategy()` on `pres` variables not yet tuned.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpholser%2Fopt-dog","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpholser%2Fopt-dog","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpholser%2Fopt-dog/lists"}