diff --git a/.ai/skills/cuopt.yaml b/.ai/skills/cuopt.yaml deleted file mode 100644 index 60ee689699..0000000000 --- a/.ai/skills/cuopt.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# cuOpt agent skill manifest (shim) -# -# Canonical location: -# - .github/.ai/skills/cuopt.yaml -# -# Many tools scan for `.ai/skills/*` at repo root; this file keeps those tools working. -manifest_version: 1 - -canonical: .github/.ai/skills/cuopt.yaml diff --git a/.github/.ai/skills/cuopt.yaml b/.github/.ai/skills/cuopt.yaml deleted file mode 100644 index 5cb35bbcb4..0000000000 --- a/.github/.ai/skills/cuopt.yaml +++ /dev/null @@ -1,85 +0,0 @@ -# cuOpt agent skill manifest (machine-readable) -manifest_version: 1 - -name: cuOpt -vendor: NVIDIA -summary: GPU-accelerated optimization engine for Routing (TSP/VRP/PDP) and Math Optimization (LP/MILP/QP). - -capabilities: - routing: - problems: [TSP, VRP, PDP] - mathematical_optimization: - problems: [LP, MILP, QP] - notes: - - QP support exists in the Python API and is currently documented as beta. - -compute: - accelerator: NVIDIA_GPU - requirements: - - CUDA 12.0+ (or CUDA 13.0+ depending on package) - - NVIDIA driver compatible with CUDA runtime - - Compute Capability >= 7.0 (Volta+) - -interfaces: - - name: python - kind: library - supports: - routing: true - lp: true - milp: true - qp: true - packages: - - cuopt-cu12 - - cuopt-cu13 - docs: - - docs/cuopt/source/cuopt-python/quick-start.rst - - docs/cuopt/source/cuopt-python/routing/index.rst - - docs/cuopt/source/cuopt-python/lp-qp-milp/index.rst - - - name: c_api - kind: library - library: libcuopt - supports: - routing: false - lp: true - milp: true - qp: true - docs: - - docs/cuopt/source/cuopt-c/index.rst - - - name: server_rest - kind: service - protocol: HTTP - supports: - routing: true - lp: true - milp: true - qp: false - implementation: - - python/cuopt_server/cuopt_server/webserver.py - openapi: - spec_file: docs/cuopt/source/cuopt_spec.yaml - served_path: /cuopt.yaml - ui_paths: - - /cuopt/docs - - /cuopt/redoc - docs: - - docs/cuopt/source/cuopt-server/quick-start.rst - - docs/cuopt/source/cuopt-server/server-api/index.rst - - docs/cuopt/source/open-api.rst - -roles: - cuopt_user: - description: Use cuOpt to solve routing and math optimization problems without modifying cuOpt internals. - rules: - - .github/agents/cuopt-user.md - cuopt_developer: - description: Develop and maintain cuOpt (C++/CUDA/Python/server/docs/CI). - rules: - - .github/agents/cuopt-developer.md - - .github/AGENTS.md - -entrypoints: - human_router: - - .github/AGENTS.md - - AGENTS.md diff --git a/.github/AGENTS.md b/.github/AGENTS.md index 04cdc37031..7854e6599f 100644 --- a/.github/AGENTS.md +++ b/.github/AGENTS.md @@ -1,15 +1,32 @@ -# AGENTS.md - AI Coding Agent Guidelines for cuOpt +# AGENTS.md - cuOpt AI Agent Entry Point -This file is intentionally **minimal**. Choose a role and follow the matching rules: +AI agent skills for NVIDIA cuOpt optimization engine. -- **Using cuOpt (model + solve problems)**: `.github/agents/cuopt-user.md` -- **Developing cuOpt (changing this repo)**: `.github/agents/cuopt-developer.md` +## Quick Start -### Machine-readable skill manifest +| Task | Read These Skills | +|------|-------------------| +| **Using cuOpt** (routing, LP, etc.) | `skills/cuopt-user-rules/` → then domain skill | +| **Developing cuOpt** (contributing) | `skills/cuopt-developer/` | -- **Primary**: `.github/.ai/skills/cuopt.yaml` -- **Root shim (compat)**: `.ai/skills/cuopt.yaml` +## Skills Directory -### Canonical contribution guide +See `skills/README.md` for the full index. -- `CONTRIBUTING.md` +### User Skills (read cuopt-user-rules first) +- `skills/cuopt-routing/` — VRP, TSP, PDP +- `skills/cuopt-lp-milp/` — Linear programming, integer variables +- `skills/cuopt-qp/` — Quadratic programming +- `skills/cuopt-debugging/` — Troubleshooting +- `skills/cuopt-installation/` — Setup & requirements +- `skills/cuopt-server/` — REST API deployment + +### Developer Skill (has its own rules) +- `skills/cuopt-developer/` — Contributing code + +## Resources + +- [cuOpt Documentation](https://docs.nvidia.com/cuopt/user-guide/latest/) +- [cuopt-examples repo](https://github.com/NVIDIA/cuopt-examples) +- [GitHub Issues](https://github.com/NVIDIA/cuopt/issues) +- [Developer Forums](https://forums.developer.nvidia.com/c/ai-data-science/nvidia-cuopt/514) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 11119becb2..cf3a570486 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -12,9 +12,18 @@ docs/ @nvidia/cuopt-infra-codeowners container-builder/ @nvidia/cuopt-infra-codeowners #CI code owners -/.github/ @nvidia/cuopt-ci-codeowners -/ci/ @nvidia/cuopt-ci-codeowners -/.pre-commit-config.yaml @nvidia/cuopt-ci-codeowners +/.github/ @nvidia/cuopt-ci-codeowners +/ci/ @nvidia/cuopt-ci-codeowners +/.pre-commit-config.yaml @nvidia/cuopt-ci-codeowners + +#agent/infra code owners (overrides .github/ for specific paths) +/AGENTS.md @nvidia/cuopt-infra-codeowners +/.ai/ @nvidia/cuopt-infra-codeowners +/.github/.coderabbit_review_guide.md @nvidia/cuopt-infra-codeowners +/.github/ISSUE_TEMPLATE/ @nvidia/cuopt-infra-codeowners +/.github/PULL_REQUEST_TEMPLATE.md @nvidia/cuopt-infra-codeowners +/.github/skills/ @nvidia/cuopt-infra-codeowners +/.github/agents-legacy/ @nvidia/cuopt-infra-codeowners #packaging code owners /.devcontainer/ @nvidia/cuopt-ci-codeowners diff --git a/.github/agents/cuopt-developer.md b/.github/agents/cuopt-developer.md deleted file mode 100644 index e426d2828a..0000000000 --- a/.github/agents/cuopt-developer.md +++ /dev/null @@ -1,184 +0,0 @@ -# cuOpt engineering contract (cuopt_developer) - -You are modifying the **cuOpt codebase**. Your priorities are correctness, performance, compatibility, and minimal-risk diffs. - -If you only need to **use** cuOpt (not change it), switch to `cuopt_user` (`.github/agents/cuopt-user.md`). - -## Project overview (developer context) - -**cuOpt** is NVIDIA's GPU-accelerated optimization engine for: - -- **Mixed Integer Linear Programming (MILP)** -- **Linear Programming (LP)** -- **Quadratic Programming (QP)** -- **Vehicle Routing Problems (VRP)** including TSP and PDP - -### Architecture (high level) - -``` -cuopt/ -├── cpp/ # Core C++ engine (libcuopt, libmps_parser) -│ ├── include/cuopt/ # Public C/C++ headers -│ ├── src/ # Implementation (CUDA kernels, algorithms) -│ └── tests/ # C++ unit tests (gtest) -├── python/ -│ ├── cuopt/ # Python bindings and routing API -│ ├── cuopt_server/ # REST API server -│ ├── cuopt_self_hosted/ # Self-hosted deployment utilities -│ └── libcuopt/ # Python wrapper for C library -├── ci/ # CI/CD scripts and Docker configurations -├── conda/ # Conda recipes and environment files -├── docs/ # Documentation source -├── datasets/ # Test datasets for LP, MIP, routing -└── notebooks/ # Example Jupyter notebooks -``` - -### Supported APIs (at a glance) - -| API Type | LP | MILP | QP | Routing | -|----------|:--:|:----:|:--:|:-------:| -| C API | ✓ | ✓ | ✓ | ✗ | -| C++ API | (internal) | (internal) | (internal) | (internal) | -| Python | ✓ | ✓ | ✓ | ✓ | -| Server | ✓ | ✓ | ✗ | ✓ | - -## Canonical project docs (source of truth) - -- **Contributing / build / test / debugging**: `CONTRIBUTING.md` -- **CI scripts**: `ci/README.md` -- **Release/version scripts**: `ci/release/README.md` -- **Documentation build**: `docs/cuopt/README.md` - -## Safety rules for agents - -- **Minimal diffs**: change only what’s necessary; avoid drive-by refactors. -- **No mass reformatting**: don’t run formatters over unrelated code. -- **No API invention**: especially for routing / server schemas—align with `docs/cuopt/source/` + OpenAPI spec. -- **Don’t bypass CI**: don’t suggest skipping checks or using `--no-verify` unless explicitly required and approved. -- **CUDA/GPU hygiene**: keep operations stream-ordered, follow existing RAFT/RMM patterns, avoid raw `new`/`delete`. - -### ⚠️ Mandatory: test impact check (ask before finalizing a change) - -Before landing any behavioral change or new feature, **explicitly ask**: - -- **What scenarios must be covered?** (happy path, edge cases, failure modes, performance regressions) -- **What’s the expected behavior contract?** (inputs/outputs, errors, compatibility constraints) -- **Where should tests live?** (C++ gtests under `cpp/tests/`, Python `pytest` under `python/.../tests`, server tests, etc.) - -**Recommendation:** add or update at least one **unit test** that covers the new behavior so **CI prevents regressions**. If full coverage isn’t feasible, document what’s untested and why, and add the smallest meaningful regression test. - -### Security bar (commands & installs) - -- **Do not run shell commands by default**: Provide commands/instructions; only execute commands if the user explicitly asks you to run them. -- **No dependency installation by default**: Don’t run `pip/conda/apt/brew` installs unless explicitly requested/approved by the user. -- **No privileged/system changes**: Never use `sudo`, modify system config, add package repositories/keys, or change driver/CUDA/toolchain setup unless explicitly requested and the implications are clear. -- **Workspace-only file changes by default**: Only create/modify files inside the checked-out repo/workspace. If writing outside the repo is necessary (e.g., under `$HOME`), ask for explicit permission and explain exactly what will be written where. -- **Prefer safe, reversible changes**: Use local envs; pin versions for reproducibility; avoid “curl | bash”. - -## Before you commit (style + signoff) - -- **Run the same style checks CI runs**: - - `./ci/check_style.sh` - - Or run pre-commit directly: `pre-commit run --all-files --show-diff-on-failure` - - Details: `CONTRIBUTING.md` (Code Formatting / pre-commit) -- **Signed commits are required (DCO sign-off)**: - - Use `git commit -s ...` (or `--signoff`) - - Details: `CONTRIBUTING.md` (Signing Your Work) - -## Coding style and conventions (summary) - -### C++ naming conventions - -- **Base style**: `snake_case` for all names (except test cases: PascalCase) -- **Prefixes/Suffixes**: - - `d_` → device data variables (e.g., `d_locations_`) - - `h_` → host data variables (e.g., `h_data_`) - - `_t` → template type parameters (e.g., `i_t`, `value_t`) - - `_` → private member variables (e.g., `n_locations_`) - -### File extensions - -| Extension | Usage | -|-----------|-------| -| `.hpp` | C++ headers | -| `.cpp` | C++ source | -| `.cu` | CUDA C++ source (nvcc required) | -| `.cuh` | CUDA headers with device code | - -### Include order - -1. Local headers -2. RAPIDS headers -3. Related libraries -4. Dependencies -5. STL - -### Python style - -- Follow PEP 8 -- Use type hints where applicable -- Tests use `pytest` framework - -### Formatting - -- **C++**: Enforced by `clang-format` (config: `cpp/.clang-format`) -- **Python**: Enforced via pre-commit hooks -- See `CONTRIBUTING.md` for pre-commit setup - -## Error handling patterns - -### Runtime assertions - -- Use `CUOPT_EXPECTS` for runtime checks -- Use `CUOPT_FAIL` for unreachable code paths - -### CUDA error checking - -- Wrap CUDA calls (e.g., `RAFT_CUDA_TRY(...)`) - -## Memory management guidelines - -- **Never use raw `new`/`delete`** - use RMM allocators -- **Prefer `rmm::device_uvector`** for device memory -- **All operations should be stream-ordered** - accept `cuda_stream_view` -- **Views (`*_view` suffix) are non-owning** - don't manage their lifetime - -## Repo navigation (practical) - -- **C++/CUDA core**: `cpp/` (includes `libmps_parser`, `libcuopt`) -- **Python packages**: `python/` (`cuopt`, `libcuopt`, `cuopt_server`, `cuopt_self_hosted`) -- **Docs (Sphinx)**: `docs/cuopt/source/` -- **Datasets**: `datasets/` - -## Build & test (quick reference; defer details to CONTRIBUTING) - -- **Build**: `./build.sh` (supports building individual components; see `./build.sh --help`) -- **C++ tests**: `ctest --test-dir cpp/build` -- **Python tests**: `pytest -v python/cuopt/cuopt/tests` (dataset env vars may be required; see `CONTRIBUTING.md`) -- **Docs build**: `./build.sh docs` or `make html` under `docs/cuopt` - -## Release discipline - -- Do not change versioning/release files unless explicitly requested. -- Prefer changes that are forward-merge friendly with RAPIDS branching conventions (see `CONTRIBUTING.md`). - -## Key files reference - -| Purpose | Location | -|---------|----------| -| Main build script | `build.sh` | -| Dependencies | `dependencies.yaml` | -| C++ formatting | `cpp/.clang-format` | -| Conda environments | `conda/environments/` | -| Test data download | `datasets/get_test_data.sh` | -| CI configuration | `ci/` | -| Version info | `VERSION` | - -## Common pitfalls - -| Problem | Solution | -|---------|----------| -| Cython changes not reflected | Rerun: `./build.sh cuopt` | -| Missing `nvcc` | Set `$CUDACXX` or add CUDA to `$PATH` | -| CUDA out of memory | Reduce problem size or use streaming | -| Slow debug library loading | Device symbols cause delay; use selectively | diff --git a/.github/agents/cuopt-user.md b/.github/agents/cuopt-user.md deleted file mode 100644 index 84d5cb2ef6..0000000000 --- a/.github/agents/cuopt-user.md +++ /dev/null @@ -1,620 +0,0 @@ -# cuOpt agent skill (cuopt_user) - -**Purpose:** Help users correctly use NVIDIA cuOpt as an end user (modeling, solving, integration), do **not** modify cuOpt internals unless explicitly asked; if you need to change cuOpt itself, switch to `cuopt_developer` (`.github/agents/cuopt-developer.md`). - ---- - -## Scope & safety rails (read first) - -This agent **assists users of cuOpt**, not cuOpt developers. -Canonical product documentation lives under `docs/cuopt/source/` (Sphinx). Prefer linking to and following those docs instead of guessing. - ---- - -## ⚠️ FIRST ACTION: Confirm the interface (mandatory unless explicit) - -**Before writing code, payloads, or implementation steps, confirm which cuOpt interface the user wants.** - -Ask the user something like: - -- **“Which interface are you using for cuOpt?”** - - **Python API** — scripts/notebooks/in-process integration - - **REST Server API** — services/microservices/production deployments - - **C API** — native C/C++ embedding - - **CLI** — quick terminal runs (typically from `.mps`) - -If your agent environment supports **multiple-choice questions**, use it. Otherwise, ask plainly in text. - -**Skip asking only if the interface is already unambiguous**, for example: - -- The user explicitly says “Python script/notebook”, “curl”, “REST endpoint”, “C API”, “cuopt_cli”, etc. -- The user provides code or payloads that clearly match one interface. -- The question is about a specific interface feature/doc path. - ---- - -## ⚠️ BEFORE WRITING CODE: Read the canonical example first (mandatory) - -After the interface is clear, **read a canonical example for that interface/problem type** and copy the pattern (imports, method names, payload structure). Do not guess API names. - -### Python API (LP/MILP/QP) agent-friendly examples (start here) - -- `.github/agents/resources/cuopt-user/python_examples.md` - -### Python API (LP/MILP/QP) canonical examples (source of truth) - -- **LP**: `docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_lp_example.py` -- **MILP**: `docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_milp_example.py` -- **QP (beta)**: `docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_qp_example.py` - -### Routing examples - -- (Agent-friendly) `.github/agents/resources/cuopt-user/python_examples.md` -- (Canonical) `docs/cuopt/source/cuopt-python/routing/examples/smoke_test_example.sh` - -### REST Server API agent-friendly examples (start here) - -- `.github/agents/resources/cuopt-user/server_examples.md` - -### REST Server API canonical sources (source of truth) - -- **OpenAPI guide**: `docs/cuopt/source/open-api.rst` -- **OpenAPI spec**: `docs/cuopt/source/cuopt_spec.yaml` (treat as the schema source-of-truth) - -### C API agent-friendly examples (start here) - -- `.github/agents/resources/cuopt-user/c_api_examples.md` - -### C API canonical sources (source of truth) - -- C API docs: `docs/cuopt/source/cuopt-c/index.rst` -- C examples: `docs/cuopt/source/cuopt-c/lp-qp-milp/examples/` - -### CLI agent-friendly examples (start here) - -- `.github/agents/resources/cuopt-user/cli_examples.md` - -### CLI canonical sources (source of truth) - -- CLI docs: `docs/cuopt/source/cuopt-cli/index.rst` -- CLI examples: `docs/cuopt/source/cuopt-cli/cli-examples.rst` - -### Interface summary - -#### Link access note (important) - -- **If the agent has the repo checked out**: local paths like `docs/cuopt/source/...` are accessible and preferred. -- **If the agent only receives this file as context (no repo access)**: prefer **public docs** and **GitHub links**: - - Official docs: [cuOpt User Guide (latest)](https://docs.nvidia.com/cuopt/user-guide/latest/introduction.html) - - Source repo: [NVIDIA/cuopt](https://github.com/NVIDIA/cuopt) - - Examples/notebooks: [NVIDIA/cuopt-examples](https://github.com/NVIDIA/cuopt-examples) - - Issues: [NVIDIA/cuopt issues](https://github.com/NVIDIA/cuopt/issues) - -If you need an online link for any local path in this document, convert it with one of these templates: - -- **GitHub (view file)**: `https://github.com/NVIDIA/cuopt/blob/main/` -- **GitHub (raw file)**: `https://raw.githubusercontent.com/NVIDIA/cuopt/main/` - -Examples: - -- `docs/cuopt/source/open-api.rst` → `https://github.com/NVIDIA/cuopt/blob/main/docs/cuopt/source/open-api.rst` -- `.github/.ai/skills/cuopt.yaml` → `https://github.com/NVIDIA/cuopt/blob/main/.github/.ai/skills/cuopt.yaml` -- `docs/cuopt/source/cuopt-python/routing/examples/smoke_test_example.sh` → `https://raw.githubusercontent.com/NVIDIA/cuopt/main/docs/cuopt/source/cuopt-python/routing/examples/smoke_test_example.sh` - -```yaml -role: cuopt_user -scope: use_cuopt_only -do_not: - - modify_cuopt_source_or_schemas - - invent_apis_or_payload_fields -repo_base: - view: https://github.com/NVIDIA/cuopt/blob/main/ - raw: https://raw.githubusercontent.com/NVIDIA/cuopt/main/ -interfaces: - c_api: - supports: {routing: false, lp: true, milp: true, qp: true} - python: - supports: {routing: true, lp: true, milp: true, qp: true} - server_rest: - supports: {routing: true, lp: true, milp: true, qp: false} - openapi_served_path: /cuopt.yaml - cli: - supports: {routing: false, lp: true, milp: true, qp: false} - mps_note: - - MPS can also be used via C API, Python API examples and via the server local-file feature; CLI is not mandatory. -escalate_to: .github/agents/cuopt-developer.md -``` - -### What cuOpt solves - -- **Routing**: TSP / VRP / PDP (GPU-accelerated) -- **Math optimization**: **LP / MILP / QP** (QP is documented as beta for the Python API) - -### DO -- **Confirm the interface first** (Python API vs REST Server vs C API vs CLI) unless the user already made it explicit. -- Help users model, solve, and integrate optimization problems using **documented cuOpt interfaces** -- Choose the **correct interface** (C API, Python API, REST server, CLI) -- Follow official documentation and examples - -### DO NOT -- Modify cuOpt internals, solver logic, schemas, or source code -- Invent APIs, fields, endpoints, or solver behaviors -- Guess payload formats or method names - -### Security bar (commands & installs) - -- **Do not run shell commands by default**: Prefer instructions and copy-pastable commands; only execute commands if the user explicitly asks you to run them. -- **No package installs by default**: Do not `pip install` / `conda install` / `apt-get install` / `brew install` unless the user explicitly requests it (or explicitly approves after you propose it). -- **No privileged/system changes**: Never use `sudo`, edit system files, add repositories/keys, or change firewall/kernel/driver settings unless the user explicitly asks and understands the impact. -- **Workspace-only file changes by default**: Only create/modify files inside the checked-out repo/workspace. If writing outside the repo is necessary (e.g., under `$HOME`), ask for explicit permission and explain exactly what will be written where. -- **Minimize risk**: Prefer user-space/virtualenv/conda environments; prefer pinned versions; avoid “curl | bash” style install instructions. - -### SWITCH TO `cuopt_developer` IF: -- User asks to change solver behavior, internals, performance heuristics -- User asks to modify OpenAPI schema or cuOpt source -- User asks to add new endpoints or features - ---- - -## Interface selection (critical) - -**🚨 STOP: Confirm the interface first (do not assume Python by default).** - -If the user didn’t explicitly specify, ask: - -- “Do you want a Python API solution, a REST Server payload/workflow, a C API embedding example, or a CLI command?” - -Proceed only after the interface is clear. - -### Interface selection workflow (decision tree) - -START → Did the user specify the interface? - -- **YES** → Use the specified interface -- **NO** → Ask which interface (Python / REST Server / C API / CLI) → Then proceed - -### ⚠️ Terminology Warning: REST vs Python API - -| Concept | REST Server API | Python API | -|---------|----------------|------------| -| Jobs/Tasks | `task_data`, `task_locations` | `set_order_locations()` | -| Time windows | `task_time_windows` | `set_order_time_windows()` | -| Service times | `service_times` | `set_order_service_times()` | - -**The REST API uses "task" terminology. The Python API uses "order" terminology.** - ---- - -## QP : critical constraints (do not miss) - -- **QP is beta** (see `docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_qp_example.py`) -- **Quadratic objectives must be MINIMIZE** (the solver rejects maximize for QP) - - **Workaround for maximization**: maximize \(f(x)\) by minimizing \(-f(x)\) -- **QP uses Barrier** internally (different from typical LP/MILP defaults) - -If a user hits an error like “Quadratic problems must be minimized”, it usually means they attempted a maximize sense with a quadratic objective. - ---- - -## Good vs bad agent behavior (interface selection) - -### ❌ Bad - -User: “Build a car rental application using cuOpt MILP.” -Agent: Immediately starts writing Python code (without confirming interface). - -### ✅ Good - -User: “Build a car rental application using cuOpt MILP.” -Agent: “Which interface do you want to use: Python API, REST Server API, C API, or CLI?” -User: “REST Server API.” -Agent: Proceeds with server deployment + request/solution workflow and validates payloads against OpenAPI. - -### Use C API when: -- User explicitly requests native integration -- User is embedding cuOpt into C/C++ systems -- **Do not** recommend the **C++ API** to end users (it is not documented and may change; see repo `README.md` note). - -➡ Use: - - C API header reference: `cpp/include/cuopt/linear_programming/cuopt_c.h` - - C overview: `docs/cuopt/source/cuopt-c/index.rst` - - C quickstart: `docs/cuopt/source/cuopt-c/quick-start.rst` - - C LP/QP/MILP API + examples: `docs/cuopt/source/cuopt-c/lp-qp-milp/index.rst` - -### Use Python API when: -- User gives equations, variables, constraints -- User wants to solve routing / LP / MILP / QP directly -- User wants in-process solving (scripts, notebooks) - -➡ Use: - - Quickstart: `docs/cuopt/source/cuopt-python/quick-start.rst` - - Routing API reference: - - `python/cuopt/cuopt/routing/vehicle_routing.py` - - `python/cuopt/cuopt/routing/assignment.py` - - `docs/cuopt/source/cuopt-python/routing/routing-api.rst` - - LP/MILP/QP API reference: - - `python/cuopt/cuopt/linear_programming/problem.py` - - `python/cuopt/cuopt/linear_programming/data_model/data_model.py` - - `python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py` - - `python/cuopt/cuopt/linear_programming/solver/solver.py` - - `docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-api.rst` - -### Use Server REST API when: -- User wants production deployment -- User asks for REST payloads or HTTP calls -- User wants asynchronous or remote solving - -➡ Use: - - Server source: `python/cuopt_server/cuopt_server/webserver.py` - - Server quickstart (includes curl smoke test): `docs/cuopt/source/cuopt-server/quick-start.rst` - - API overview: `docs/cuopt/source/cuopt-server/server-api/index.rst` - - OpenAPI reference (Swagger): `docs/cuopt/source/open-api.rst` - - OpenAPI spec exactly (`cuopt.yaml` / `cuopt_spec.yaml`) - -### Use CLI when: -- User wants **quick testing** / **research** / **reproducible debugging** from a terminal -- User wants to solve **LP/MILP from MPS files** without writing code - -➡ Use: - - CLI source: `cpp/cuopt_cli.cpp` - - CLI overview: `docs/cuopt/source/cuopt-cli/index.rst` - - CLI quickstart: `docs/cuopt/source/cuopt-cli/quick-start.rst` - - CLI examples: `docs/cuopt/source/cuopt-cli/cli-examples.rst` - -**Note on MPS inputs:** having an `.mps` file does **not** imply you must use the CLI. -Choose based on integration/deployment needs: - -- **CLI**: fastest local repro (LP/MILP from MPS) -- **C API**: native embedding; includes MPS-based examples under `docs/cuopt/source/cuopt-c/lp-qp-milp/examples/` -- **Server**: can use its local-file feature (see server docs/OpenAPI) when running a service - ---- - -## ⚠️ Status Checking (Critical for LP/MILP) - -**Status enum values use PascalCase, not ALL_CAPS.** - -| Correct | Wrong | -|---------|-------| -| `"Optimal"` | `"OPTIMAL"` | -| `"FeasibleFound"` | `"FEASIBLE"` | -| `"Infeasible"` | `"INFEASIBLE"` | - -**Always check status like this:** - -```python -# ✅ CORRECT - matches actual enum names -if problem.Status.name in ["Optimal", "FeasibleFound"]: - print(f"Solution: {problem.ObjValue}") - -# ✅ ALSO CORRECT - case-insensitive -if problem.Status.name.upper() == "OPTIMAL": - print(f"Solution: {problem.ObjValue}") - -# ❌ WRONG - will silently fail! -if problem.Status.name == "OPTIMAL": # Never matches - print(f"Solution: {problem.ObjValue}") -``` - -**LP status values:** `Optimal`, `NoTermination`, `NumericalError`, `PrimalInfeasible`, `DualInfeasible`, `IterationLimit`, `TimeLimit`, `PrimalFeasible` - -**MILP status values:** `Optimal`, `FeasibleFound`, `Infeasible`, `Unbounded`, `TimeLimit`, `NoTermination` - ---- - -## Installation (minimal) - -Pick **one** installation method and match it to your CUDA major version (cuOpt publishes CUDA-variant packages). - -### pip - -- **Python API**: - -```bash -# Simplest (latest compatible from the index): -# CUDA 13 -pip install --extra-index-url=https://pypi.nvidia.com cuopt-cu13 - -# CUDA 12 -pip install --extra-index-url=https://pypi.nvidia.com cuopt-cu12 - -# Recommended (reproducible; pin to the current major/minor release line): -# CUDA 13 -pip install --extra-index-url=https://pypi.nvidia.com 'cuopt-cu13==26.2.*' - -# CUDA 12 -pip install --extra-index-url=https://pypi.nvidia.com 'cuopt-cu12==26.2.*' -``` - -- **Server + thin client (self-hosted)**: - -```bash -# Simplest: -# CUDA 12 example -pip install --extra-index-url=https://pypi.nvidia.com \ - cuopt-server-cu12 cuopt-sh-client - -# Recommended (reproducible): -# CUDA 12 example -pip install --extra-index-url=https://pypi.nvidia.com \ - nvidia-cuda-runtime-cu12==12.9.* \ - cuopt-server-cu12==26.02.* cuopt-sh-client==26.02.* -``` - -### conda - -```bash -# Simplest: -# Python API -conda install -c rapidsai -c conda-forge -c nvidia cuopt - -# Server + thin client -conda install -c rapidsai -c conda-forge -c nvidia cuopt-server cuopt-sh-client - -# Recommended (reproducible): -# Python API -conda install -c rapidsai -c conda-forge -c nvidia cuopt=26.02.* cuda-version=26.02.* - -# Server + thin client -conda install -c rapidsai -c conda-forge -c nvidia cuopt-server=26.02.* cuopt-sh-client=26.02.* -``` - -### container - -```bash -docker pull nvidia/cuopt:latest-cuda12.9-py3.13 -docker run --gpus all -it --rm -p 8000:8000 -e CUOPT_SERVER_PORT=8000 nvidia/cuopt:latest-cuda12.9-py3.13 -``` - -For full, up-to-date installation instructions (including nightlies), see: - -- `docs/cuopt/source/cuopt-python/quick-start.rst` -- `docs/cuopt/source/cuopt-server/quick-start.rst` - ---- - -## C API Examples & Templates - -Use the C examples + Makefile under `docs/cuopt/source/cuopt-c/lp-qp-milp/examples/`. - -Long-form examples (kept out of this doc): - -- `.github/agents/resources/cuopt-user/c_api_examples.md` - ---- - -## Python API Examples & Templates - -Long-form examples (routing + LP/MILP/QP; kept out of this doc): - -- `.github/agents/resources/cuopt-user/python_examples.md` - ---- - -## Server REST API Examples & Templates - -Long-form examples (kept out of this doc): - -- `.github/agents/resources/cuopt-user/server_examples.md` - ---- - -## CLI Examples & Templates - -Long-form examples (kept out of this doc): - -- `.github/agents/resources/cuopt-user/cli_examples.md` - ---- - -## Debugging Checklist - -### Problem: Results are empty/None when status looks OK - -**Diagnosis:** -```python -# Check actual status string (case matters!) -print(f"Status: '{problem.Status.name}'") -print(f"Is 'Optimal'?: {problem.Status.name == 'Optimal'}") -print(f"Is 'OPTIMAL'?: {problem.Status.name == 'OPTIMAL'}") # Wrong case! -``` - -**Fix:** Use `status in ["Optimal", "FeasibleFound"]` not `status == "OPTIMAL"` - -### Problem: Objective near zero when expecting large value - -**Diagnosis:** -```python -# Check if variables are all zero -for var in [var1, var2, var3]: - print(f"{var.name}: {var.getValue()}") -print(f"ObjValue: {problem.ObjValue}") -``` - -**Common causes:** -- Model formulation error (constraints too restrictive) -- Objective coefficients have wrong sign -- "Do nothing" is optimal (check constraint logic) - -### Problem: Integer variables have fractional values - -**Diagnosis:** -```python -# Verify variable was defined as INTEGER -val = int_var.getValue() -print(f"Value: {val}, Is integer?: {abs(val - round(val)) < 1e-6}") -``` - -**Common causes:** -- Variable defined as `CONTINUOUS` instead of `INTEGER` -- Solver hit time limit before finding integer solution (check `FeasibleFound` vs `Optimal`) - -### Problem: Routing solution empty or status != 0 - -**Diagnosis:** -```python -print(f"Status: {solution.get_status()}") # 0=SUCCESS, 1=FAIL, 2=TIMEOUT, 3=EMPTY -print(f"Message: {solution.get_message()}") -print(f"Error: {solution.get_error_message()}") - -# Check for dropped/infeasible orders -infeasible = solution.get_infeasible_orders() -if len(infeasible) > 0: - print(f"Infeasible orders: {infeasible.to_list()}") -``` - -**Common causes:** -- Time windows too tight (order earliest > vehicle latest) -- Total demand exceeds total capacity -- Cost/time matrix dimensions don't match n_locations -- Missing `add_transit_time_matrix()` when using time windows - -### Problem: Server REST API returns 422 validation error - -**Diagnosis:** -- Check the `error` field in response for specific validation message -- Common issues: - - `transit_time_matrix_data` → should be `travel_time_matrix_data` - - `capacities` format: `[[cap_v1, cap_v2]]` not `[[cap_v1], [cap_v2]]` - - Missing required fields: `fleet_data`, `task_data` - -**Fix:** Compare payload against OpenAPI spec at `/cuopt.yaml` - -### Problem: OutOfMemoryError - -**Diagnosis:** -```python -# Check problem size -print(f"Variables: {problem.num_variables}") -print(f"Constraints: {problem.num_constraints}") -# For routing: -print(f"Locations: {n_locations}, Orders: {n_orders}, Fleet: {n_fleet}") -``` - -**Common causes:** -- Problem too large for GPU memory -- Dense constraint matrix (try sparse representation) -- Too many vehicles × locations in routing - -### Problem: cudf type casting warnings or errors - -**Diagnosis:** -```python -# Check dtypes before passing to cuOpt -print(f"cost_matrix dtype: {cost_matrix.dtypes}") -print(f"demand dtype: {demand.dtype}") -``` - -**Fix:** Explicitly cast to expected types: -```python -cost_matrix = cost_matrix.astype("float32") -demand = demand.astype("int32") -order_locations = order_locations.astype("int32") -``` - -### Problem: MPS file parsing fails - -**Diagnosis:** -```bash -# Check MPS file format -head -20 problem.mps -# Look for: NAME, ROWS, COLUMNS, RHS, BOUNDS, ENDATA sections -``` - -**Common causes:** -- Missing `ENDATA` marker -- Incorrect section order -- Invalid characters or encoding -- Integer markers (`'MARKER'`, `'INTORG'`, `'INTEND'`) malformed - -### Problem: Time windows make problem infeasible - -**Diagnosis:** -```python -# Check for impossible time windows -for i in range(len(order_earliest)): - if order_earliest[i] > order_latest[i]: - print(f"Order {i}: earliest {order_earliest[i]} > latest {order_latest[i]}") - -# Check vehicle can reach orders in time -for i in range(len(order_locations)): - loc = order_locations[i] - travel_time = transit_time_matrix[0][loc] # from depot - if travel_time > order_latest[i]: - print(f"Order {i}: unreachable (travel={travel_time}, latest={order_latest[i]})") -``` - ---- - -## Common user requests → action map - -| User asks | First action | Then | -|----------|--------| -| "Build an optimization app" | **Ask which interface** (Python / REST / C / CLI) | Implement in the chosen interface | -| "Embed cuOpt in C/C++ app" | Confirm they want **C API** | Use C API docs/examples | -| "Solve this routing problem" | Ask **Python vs REST** (unless explicit) | Use routing API / server payloads accordingly | -| "Solve this LP/MILP" | Ask **Python vs REST vs C vs CLI** (unless explicit) | Use the chosen interface | -| "Write a Python script to..." | Use **Python API** | Implement the script | -| "Give REST payload" / provides `curl` | Use **REST Server API** | Validate against OpenAPI spec | -| "I have MPS file" | Ask **CLI vs embedding/service** | CLI for quick repro **or** C API MPS examples **or** Server local-file feature | -| "422 / schema error" | Use **REST Server API** | Fix payload using OpenAPI spec | -| "Solver too slow" | Confirm interface + constraints | Adjust documented settings (time limits, gaps, etc.) | -| "Change solver logic" | Switch to `cuopt_developer` | Modify codebase per dev rules | - ---- - -## Solver settings (safe adjustments) - -Allowed: -- Time limit -- Gap tolerances (if documented) -- Verbosity / logging - -Not allowed: -- Changing heuristics -- Modifying internals -- Undocumented parameters - ---- - -## Data formats & performance - -- **Payload formats**: JSON is the default; msgpack/zlib are supported for some endpoints (see server docs/OpenAPI). -- **GPU constraints**: requires a supported NVIDIA GPU/driver/CUDA runtime; see the system requirements in the main README and docs. -- **Tuning**: use solver settings (e.g., time limits) and avoid unnecessary host↔device churn; follow the feature docs under `docs/cuopt/source/`. - ---- - -## Error handling (agent rules) - -- **Validation errors (HTTP 4xx)**: treat as schema/typing issues; consult OpenAPI spec and fix the request payload. -- **Server errors (HTTP 5xx)**: capture `reqId`, poll logs/status endpoints where applicable, and reproduce with the smallest request. -- **Never "paper over" errors** by changing schemas or endpoints—align with the documented API. -- **Debugging a failure**: search existing [GitHub Issues](https://github.com/NVIDIA/cuopt/issues) first (use exact error text + cuOpt/CUDA/driver versions). If no match, file a new issue with a minimal repro, expected vs actual behavior, environment details, and any logs/`reqId`. - -For common troubleshooting and known issues, see: - -- `docs/cuopt/source/faq.rst` -- `docs/cuopt/source/resources.rst` - ---- - -## Additional resources (when to use) - -- **Examples / notebooks**: [NVIDIA/cuopt-examples](https://github.com/NVIDIA/cuopt-examples) → runnable notebooks -- **Google Colab**: [cuopt-examples notebooks on Colab](https://colab.research.google.com/github/nvidia/cuopt-examples/) → runnable examples -- **Official docs**: [cuOpt User Guide](https://docs.nvidia.com/cuopt/user-guide/latest/introduction.html) → modeling correctness -- **Videos/tutorials**: [cuOpt examples and tutorials videos](https://docs.nvidia.com/cuopt/user-guide/latest/resources.html#cuopt-examples-and-tutorials-videos) → unclear behavior -- **Try in the cloud**: [NVIDIA Launchable](https://brev.nvidia.com/launchable/deploy?launchableID=env-2qIG6yjGKDtdMSjXHcuZX12mDNJ) → GPU environments -- **Support / questions**: [NVIDIA Developer Forums (cuOpt)](https://forums.developer.nvidia.com/c/ai-data-science/nvidia-cuopt/514) → unclear behavior -- **Bugs / feature requests**: [GitHub Issues](https://github.com/NVIDIA/cuopt/issues) → unclear behavior - ---- - -## Final agent rules (non-negotiable) - -- Never invent APIs -- Never assume undocumented behavior -- Always choose interface first -- Prefer correctness over speed -- When unsure → open docs or ask user to clarify diff --git a/.github/agents/resources/cuopt-user/README.md b/.github/agents/resources/cuopt-user/README.md deleted file mode 100644 index 15ec4a7aa2..0000000000 --- a/.github/agents/resources/cuopt-user/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# cuopt-user resources - -This folder contains **long-form examples** referenced by `.github/agents/cuopt-user.md`. - -They are kept out of the main agent doc to reduce prompt bloat in environments that eagerly ingest Markdown files. - -Contents: - -- `c_api_examples.md` — C API examples + build/run notes -- `python_examples.md` — Python API examples (routing + LP/MILP/QP) -- `server_examples.md` — REST server examples (start server, curl, Python `requests`) -- `cli_examples.md` — CLI examples (MPS creation + solve commands) diff --git a/.github/agents/resources/cuopt-user/c_api_examples.md b/.github/agents/resources/cuopt-user/c_api_examples.md deleted file mode 100644 index f42dcd5d3a..0000000000 --- a/.github/agents/resources/cuopt-user/c_api_examples.md +++ /dev/null @@ -1,245 +0,0 @@ -# C API examples (cuOpt) - -## C API: Simple LP Example - -```c -/* - * Simple LP C API Example - * - * Solve: minimize -0.2*x1 + 0.1*x2 - * subject to 3.0*x1 + 4.0*x2 <= 5.4 - * 2.7*x1 + 10.1*x2 <= 4.9 - * x1, x2 >= 0 - * - * Expected: x1 = 1.8, x2 = 0.0, objective = -0.36 - */ -#include -#include -#include - -int main() { - cuOptOptimizationProblem problem = NULL; - cuOptSolverSettings settings = NULL; - cuOptSolution solution = NULL; - - cuopt_int_t num_variables = 2; - cuopt_int_t num_constraints = 2; - - // Constraint matrix in CSR format - cuopt_int_t row_offsets[] = {0, 2, 4}; - cuopt_int_t column_indices[] = {0, 1, 0, 1}; - cuopt_float_t values[] = {3.0, 4.0, 2.7, 10.1}; - - // Objective coefficients: minimize -0.2*x1 + 0.1*x2 - cuopt_float_t objective_coefficients[] = {-0.2, 0.1}; - - // Constraint bounds (ranged form: lower <= Ax <= upper) - cuopt_float_t constraint_upper_bounds[] = {5.4, 4.9}; - cuopt_float_t constraint_lower_bounds[] = {-CUOPT_INFINITY, -CUOPT_INFINITY}; - - // Variable bounds: x1, x2 >= 0 - cuopt_float_t var_lower_bounds[] = {0.0, 0.0}; - cuopt_float_t var_upper_bounds[] = {CUOPT_INFINITY, CUOPT_INFINITY}; - - // Variable types: both continuous - char variable_types[] = {CUOPT_CONTINUOUS, CUOPT_CONTINUOUS}; - - cuopt_int_t status; - cuopt_float_t time; - cuopt_int_t termination_status; - cuopt_float_t objective_value; - - // Create the problem - status = cuOptCreateRangedProblem( - num_constraints, - num_variables, - CUOPT_MINIMIZE, - 0.0, // objective offset - objective_coefficients, - row_offsets, - column_indices, - values, - constraint_lower_bounds, - constraint_upper_bounds, - var_lower_bounds, - var_upper_bounds, - variable_types, - &problem - ); - if (status != CUOPT_SUCCESS) { - printf("Error creating problem: %d\n", status); - return 1; - } - - // Create solver settings - status = cuOptCreateSolverSettings(&settings); - if (status != CUOPT_SUCCESS) { - printf("Error creating solver settings: %d\n", status); - goto DONE; - } - - // Set solver parameters - cuOptSetFloatParameter(settings, CUOPT_ABSOLUTE_PRIMAL_TOLERANCE, 0.0001); - cuOptSetFloatParameter(settings, CUOPT_TIME_LIMIT, 60.0); - - // Solve the problem - status = cuOptSolve(problem, settings, &solution); - if (status != CUOPT_SUCCESS) { - printf("Error solving problem: %d\n", status); - goto DONE; - } - - // Get and print results - cuOptGetSolveTime(solution, &time); - cuOptGetTerminationStatus(solution, &termination_status); - cuOptGetObjectiveValue(solution, &objective_value); - - printf("Termination status: %d\n", termination_status); - printf("Solve time: %f seconds\n", time); - printf("Objective value: %f\n", objective_value); - - // Get solution values - cuopt_float_t* solution_values = (cuopt_float_t*)malloc( - num_variables * sizeof(cuopt_float_t) - ); - cuOptGetPrimalSolution(solution, solution_values); - for (cuopt_int_t i = 0; i < num_variables; i++) { - printf("x%d = %f\n", i + 1, solution_values[i]); - } - free(solution_values); - -DONE: - cuOptDestroyProblem(&problem); - cuOptDestroySolverSettings(&settings); - cuOptDestroySolution(&solution); - - return (status == CUOPT_SUCCESS) ? 0 : 1; -} -``` - -## C API: MILP Example (with integer variables) - -```c -/* - * Simple MILP C API Example - * - * Solve: minimize -0.2*x1 + 0.1*x2 - * subject to 3.0*x1 + 4.0*x2 <= 5.4 - * 2.7*x1 + 10.1*x2 <= 4.9 - * x1 integer, x2 continuous, both >= 0 - */ -#include -#include -#include - -int main() { - cuOptOptimizationProblem problem = NULL; - cuOptSolverSettings settings = NULL; - cuOptSolution solution = NULL; - - cuopt_int_t num_variables = 2; - cuopt_int_t num_constraints = 2; - - // Constraint matrix in CSR format - cuopt_int_t row_offsets[] = {0, 2, 4}; - cuopt_int_t column_indices[] = {0, 1, 0, 1}; - cuopt_float_t values[] = {3.0, 4.0, 2.7, 10.1}; - - // Objective coefficients - cuopt_float_t objective_coefficients[] = {-0.2, 0.1}; - - // Constraint bounds - cuopt_float_t constraint_upper_bounds[] = {5.4, 4.9}; - cuopt_float_t constraint_lower_bounds[] = {-CUOPT_INFINITY, -CUOPT_INFINITY}; - - // Variable bounds - cuopt_float_t var_lower_bounds[] = {0.0, 0.0}; - cuopt_float_t var_upper_bounds[] = {CUOPT_INFINITY, CUOPT_INFINITY}; - - // Variable types: x1 = INTEGER, x2 = CONTINUOUS - char variable_types[] = {CUOPT_INTEGER, CUOPT_CONTINUOUS}; - - cuopt_int_t status; - cuopt_float_t time; - cuopt_int_t termination_status; - cuopt_float_t objective_value; - - // Create the problem (same API, but with integer variable types) - status = cuOptCreateRangedProblem( - num_constraints, - num_variables, - CUOPT_MINIMIZE, - 0.0, - objective_coefficients, - row_offsets, - column_indices, - values, - constraint_lower_bounds, - constraint_upper_bounds, - var_lower_bounds, - var_upper_bounds, - variable_types, - &problem - ); - if (status != CUOPT_SUCCESS) { - printf("Error creating problem: %d\n", status); - return 1; - } - - // Create solver settings - status = cuOptCreateSolverSettings(&settings); - if (status != CUOPT_SUCCESS) goto DONE; - - // Set MIP-specific parameters - cuOptSetFloatParameter(settings, CUOPT_MIP_ABSOLUTE_TOLERANCE, 0.0001); - cuOptSetFloatParameter(settings, CUOPT_MIP_RELATIVE_GAP, 0.01); // 1% gap - cuOptSetFloatParameter(settings, CUOPT_TIME_LIMIT, 120.0); - - // Solve - status = cuOptSolve(problem, settings, &solution); - if (status != CUOPT_SUCCESS) goto DONE; - - // Get results - cuOptGetSolveTime(solution, &time); - cuOptGetTerminationStatus(solution, &termination_status); - cuOptGetObjectiveValue(solution, &objective_value); - - printf("Termination status: %d\n", termination_status); - printf("Solve time: %f seconds\n", time); - printf("Objective value: %f\n", objective_value); - - cuopt_float_t* solution_values = malloc(num_variables * sizeof(cuopt_float_t)); - cuOptGetPrimalSolution(solution, solution_values); - printf("x1 (integer) = %f\n", solution_values[0]); - printf("x2 (continuous) = %f\n", solution_values[1]); - free(solution_values); - -DONE: - cuOptDestroyProblem(&problem); - cuOptDestroySolverSettings(&settings); - cuOptDestroySolution(&solution); - - return (status == CUOPT_SUCCESS) ? 0 : 1; -} -``` - -## C API: Build & Run - -```bash -# Find include and library paths (adjust based on installation) -# If installed via conda: -export INCLUDE_PATH="${CONDA_PREFIX}/include" -export LIBCUOPT_LIBRARY_PATH="${CONDA_PREFIX}/lib" - -# Or find automatically: -# INCLUDE_PATH=$(find / -name "cuopt_c.h" -path "*/linear_programming/*" \ -# -printf "%h\n" | sed 's/\/linear_programming//' 2>/dev/null) -# LIBCUOPT_LIBRARY_PATH=$(dirname $(find / -name "libcuopt.so" 2>/dev/null)) - -# Compile -gcc -I ${INCLUDE_PATH} -L ${LIBCUOPT_LIBRARY_PATH} \ - -o simple_lp_example simple_lp_example.c -lcuopt - -# Run -LD_LIBRARY_PATH=${LIBCUOPT_LIBRARY_PATH}:$LD_LIBRARY_PATH ./simple_lp_example -``` diff --git a/.github/agents/resources/cuopt-user/cli_examples.md b/.github/agents/resources/cuopt-user/cli_examples.md deleted file mode 100644 index 020ca0046c..0000000000 --- a/.github/agents/resources/cuopt-user/cli_examples.md +++ /dev/null @@ -1,106 +0,0 @@ -# CLI examples (cuOpt) - -## CLI: LP from MPS File - -```bash -# Create sample LP problem in MPS format -cat > production.mps << 'EOF' -* Production Planning Problem -* maximize 40*chairs + 30*tables -* s.t. 2*chairs + 3*tables <= 240 (wood) -* 4*chairs + 2*tables <= 200 (labor) -NAME PRODUCTION -ROWS - N PROFIT - L WOOD - L LABOR -COLUMNS - CHAIRS PROFIT -40.0 - CHAIRS WOOD 2.0 - CHAIRS LABOR 4.0 - TABLES PROFIT -30.0 - TABLES WOOD 3.0 - TABLES LABOR 2.0 -RHS - RHS1 WOOD 240.0 - RHS1 LABOR 200.0 -ENDATA -EOF - -# Solve with cuopt_cli -cuopt_cli production.mps - -# Solve with options -cuopt_cli production.mps --time-limit 30 - -# Cleanup -rm -f production.mps -``` - -## CLI: MILP from MPS File - -```bash -# Create MILP problem (with integer variables) -cat > facility.mps << 'EOF' -* Facility location - simplified -* Binary variables for opening facilities -NAME FACILITY -ROWS - N COST - G DEMAND1 - L CAP1 - L CAP2 -COLUMNS - MARKER 'MARKER' 'INTORG' - OPEN1 COST 100.0 - OPEN1 CAP1 50.0 - OPEN2 COST 150.0 - OPEN2 CAP2 70.0 - MARKER 'MARKER' 'INTEND' - SHIP11 COST 5.0 - SHIP11 DEMAND1 1.0 - SHIP11 CAP1 -1.0 - SHIP21 COST 7.0 - SHIP21 DEMAND1 1.0 - SHIP21 CAP2 -1.0 -RHS - RHS1 DEMAND1 30.0 -BOUNDS - BV BND1 OPEN1 - BV BND1 OPEN2 - LO BND1 SHIP11 0.0 - LO BND1 SHIP21 0.0 -ENDATA -EOF - -# Solve MILP -cuopt_cli facility.mps --time-limit 60 --mip-relative-tolerance 0.01 - -# Cleanup -rm -f facility.mps -``` - -## CLI: Common Options - -```bash -# Show all options -cuopt_cli --help - -# Set time limit (seconds) -cuopt_cli problem.mps --time-limit 120 - -# Set MIP relative gap tolerance (for MILP, e.g., 0.1% = 0.001) -cuopt_cli problem.mps --mip-relative-tolerance 0.001 - -# Set MIP absolute tolerance (for MILP) -cuopt_cli problem.mps --mip-absolute-tolerance 0.0001 - -# Enable presolve -cuopt_cli problem.mps --presolve - -# Set iteration limit -cuopt_cli problem.mps --iteration-limit 10000 - -# Specify solver method (0=auto, 1=pdlp, 2=dual_simplex, 3=barrier, etc.) -cuopt_cli problem.mps --method 1 -``` diff --git a/.github/agents/resources/cuopt-user/python_examples.md b/.github/agents/resources/cuopt-user/python_examples.md deleted file mode 100644 index ee2d4ee201..0000000000 --- a/.github/agents/resources/cuopt-user/python_examples.md +++ /dev/null @@ -1,381 +0,0 @@ -# Python API examples (cuOpt) - -## Python: Routing with Time Windows & Capacities (VRP) - -```python -""" -Vehicle Routing Problem with: -- 1 depot (location 0) -- 5 customer locations (1-5) -- 2 vehicles with capacity 100 each -- Time windows for each location -- Demand at each customer -""" -import cudf -from cuopt import routing - -# Cost/distance matrix (6x6: depot + 5 customers) -cost_matrix = cudf.DataFrame([ - [0, 10, 15, 20, 25, 30], # From depot - [10, 0, 12, 18, 22, 28], # From customer 1 - [15, 12, 0, 10, 15, 20], # From customer 2 - [20, 18, 10, 0, 8, 15], # From customer 3 - [25, 22, 15, 8, 0, 10], # From customer 4 - [30, 28, 20, 15, 10, 0], # From customer 5 -], dtype="float32") - -# Also use as transit time matrix (same values for simplicity) -transit_time_matrix = cost_matrix.copy(deep=True) - -# Order data (customers 1-5) -order_locations = cudf.Series([1, 2, 3, 4, 5]) # Location indices for orders - -# Demand at each customer (single capacity dimension) -demand = cudf.Series([20, 30, 25, 15, 35], dtype="int32") # Units to deliver - -# Vehicle capacities (must match demand dimensions) -vehicle_capacity = cudf.Series([100, 100], dtype="int32") # Each vehicle can carry 100 units - -# Time windows for orders [earliest, latest] -order_earliest = cudf.Series([0, 10, 20, 0, 30], dtype="int32") -order_latest = cudf.Series([50, 60, 70, 80, 90], dtype="int32") - -# Service time at each customer -service_times = cudf.Series([5, 5, 5, 5, 5], dtype="int32") - -# Fleet configuration -n_fleet = 2 - -# Vehicle start/end locations (both start and return to depot) -vehicle_start = cudf.Series([0, 0], dtype="int32") -vehicle_end = cudf.Series([0, 0], dtype="int32") - -# Vehicle time windows (operating hours) -vehicle_earliest = cudf.Series([0, 0], dtype="int32") -vehicle_latest = cudf.Series([200, 200], dtype="int32") - -# Build the data model -dm = routing.DataModel( - n_locations=cost_matrix.shape[0], - n_fleet=n_fleet, - n_orders=len(order_locations) -) - -# Add matrices -dm.add_cost_matrix(cost_matrix) -dm.add_transit_time_matrix(transit_time_matrix) - -# Add order data -dm.set_order_locations(order_locations) -dm.set_order_time_windows(order_earliest, order_latest) -dm.set_order_service_times(service_times) - -# Add capacity dimension (name, demand_per_order, capacity_per_vehicle) -dm.add_capacity_dimension("weight", demand, vehicle_capacity) - -# Add fleet data -dm.set_vehicle_locations(vehicle_start, vehicle_end) -dm.set_vehicle_time_windows(vehicle_earliest, vehicle_latest) - -# Configure solver -ss = routing.SolverSettings() -ss.set_time_limit(10) # seconds - -# Solve -solution = routing.Solve(dm, ss) - -# Check solution status -print(f"Status: {solution.get_status()}") - -# Display routes -if solution.get_status() == 0: # Success - print("\n--- Solution Found ---") - solution.display_routes() - - # Get detailed route data - route_df = solution.get_route() - print("\nDetailed route data:") - print(route_df) - - # Get objective value (total cost) - print(f"\nTotal cost: {solution.get_total_objective()}") -else: - print("No feasible solution found (status != 0).") -``` - -## Python: Pickup and Delivery Problem (PDP) - -```python -""" -Pickup and Delivery Problem: -- Items must be picked up from one location and delivered to another -- Same vehicle must do both pickup and delivery -- Pickup must occur before delivery -""" -import cudf -from cuopt import routing - -# Cost matrix (depot + 4 locations) -cost_matrix = cudf.DataFrame([ - [0, 10, 20, 30, 40], - [10, 0, 15, 25, 35], - [20, 15, 0, 10, 20], - [30, 25, 10, 0, 15], - [40, 35, 20, 15, 0], -], dtype="float32") - -transit_time_matrix = cost_matrix.copy(deep=True) - -n_fleet = 2 -n_orders = 4 # 2 pickup-delivery pairs = 4 orders - -# Orders: pickup at loc 1 -> deliver at loc 2, pickup at loc 3 -> deliver at loc 4 -order_locations = cudf.Series([1, 2, 3, 4]) - -# Pickup and delivery pairs (indices into order array) -# Order 0 (pickup) pairs with Order 1 (delivery) -# Order 2 (pickup) pairs with Order 3 (delivery) -pickup_indices = cudf.Series([0, 2]) -delivery_indices = cudf.Series([1, 3]) - -# Demand: positive for pickup, negative for delivery (must sum to 0 per pair) -demand = cudf.Series([10, -10, 15, -15], dtype="int32") -vehicle_capacity = cudf.Series([50, 50], dtype="int32") - -# Build model -dm = routing.DataModel( - n_locations=cost_matrix.shape[0], - n_fleet=n_fleet, - n_orders=n_orders -) - -dm.add_cost_matrix(cost_matrix) -dm.add_transit_time_matrix(transit_time_matrix) -dm.set_order_locations(order_locations) - -# Add capacity dimension -dm.add_capacity_dimension("load", demand, vehicle_capacity) - -# Set pickup and delivery constraints -dm.set_pickup_delivery_pairs(pickup_indices, delivery_indices) - -# Fleet setup -dm.set_vehicle_locations( - cudf.Series([0, 0]), # Start at depot - cudf.Series([0, 0]) # Return to depot -) - -# Solve -ss = routing.SolverSettings() -ss.set_time_limit(10) -solution = routing.Solve(dm, ss) - -print(f"Status: {solution.get_status()}") -if solution.get_status() == 0: - solution.display_routes() -``` - -## Python: Linear Programming (LP) - -```python -""" -Production Planning LP: - maximize 40*chairs + 30*tables (profit) - subject to 2*chairs + 3*tables <= 240 (wood constraint) - 4*chairs + 2*tables <= 200 (labor constraint) - chairs, tables >= 0 -""" -from cuopt.linear_programming.problem import Problem, CONTINUOUS, MAXIMIZE -from cuopt.linear_programming.solver_settings import SolverSettings - -# Create problem -problem = Problem("ProductionPlanning") - -# Decision variables (continuous, non-negative) -chairs = problem.addVariable(lb=0, vtype=CONTINUOUS, name="chairs") -tables = problem.addVariable(lb=0, vtype=CONTINUOUS, name="tables") - -# Constraints -problem.addConstraint(2 * chairs + 3 * tables <= 240, name="wood") -problem.addConstraint(4 * chairs + 2 * tables <= 200, name="labor") - -# Objective: maximize profit -problem.setObjective(40 * chairs + 30 * tables, sense=MAXIMIZE) - -# Solver settings -settings = SolverSettings() -settings.set_parameter("time_limit", 60) -settings.set_parameter("log_to_console", 1) - -# Solve -problem.solve(settings) - -# Check status and extract results -status = problem.Status.name -print(f"Status: {status}") - -if status in ["Optimal", "PrimalFeasible"]: - print(f"Optimal profit: ${problem.ObjValue:.2f}") - print(f"Chairs to produce: {chairs.getValue():.1f}") - print(f"Tables to produce: {tables.getValue():.1f}") - - # Get dual values (shadow prices) - wood_constraint = problem.getConstraint("wood") - labor_constraint = problem.getConstraint("labor") - print(f"\nShadow price (wood): ${wood_constraint.DualValue:.2f} per unit") - print(f"Shadow price (labor): ${labor_constraint.DualValue:.2f} per unit") -else: - print(f"No optimal solution found. Status: {status}") -``` - -## Python: Mixed-Integer Linear Programming (MILP) - -```python -""" -Facility Location MILP: -- Decide which warehouses to open (binary) -- Assign customers to open warehouses -- Minimize fixed costs + transportation costs -""" -from cuopt.linear_programming.problem import ( - Problem, CONTINUOUS, INTEGER, MINIMIZE -) -from cuopt.linear_programming.solver_settings import SolverSettings - -# Problem data -warehouses = ["W1", "W2", "W3"] -customers = ["C1", "C2", "C3", "C4"] - -fixed_costs = {"W1": 100, "W2": 150, "W3": 120} -capacities = {"W1": 50, "W2": 70, "W3": 60} -demands = {"C1": 20, "C2": 25, "C3": 15, "C4": 30} - -# Transportation cost from warehouse to customer -transport_cost = { - ("W1", "C1"): 5, ("W1", "C2"): 8, ("W1", "C3"): 6, ("W1", "C4"): 10, - ("W2", "C1"): 7, ("W2", "C2"): 4, ("W2", "C3"): 9, ("W2", "C4"): 5, - ("W3", "C1"): 6, ("W3", "C2"): 7, ("W3", "C3"): 4, ("W3", "C4"): 8, -} - -# Create problem -problem = Problem("FacilityLocation") - -# Decision variables -# y[w] = 1 if warehouse w is open (binary: INTEGER with bounds 0-1) -y = {w: problem.addVariable(lb=0, ub=1, vtype=INTEGER, name=f"open_{w}") for w in warehouses} - -# x[w,c] = units shipped from w to c -x = { - (w, c): problem.addVariable(lb=0, vtype=CONTINUOUS, name=f"ship_{w}_{c}") - for w in warehouses for c in customers -} - -# Objective: minimize fixed + transportation costs -problem.setObjective( - sum(fixed_costs[w] * y[w] for w in warehouses) + - sum(transport_cost[w, c] * x[w, c] for w in warehouses for c in customers), - sense=MINIMIZE -) - -# Constraints -# 1. Meet customer demand -for c in customers: - problem.addConstraint( - sum(x[w, c] for w in warehouses) == demands[c], - name=f"demand_{c}" - ) - -# 2. Respect warehouse capacity (only if open) -for w in warehouses: - problem.addConstraint( - sum(x[w, c] for c in customers) <= capacities[w] * y[w], - name=f"capacity_{w}" - ) - -# Solver settings -settings = SolverSettings() -settings.set_parameter("time_limit", 120) -settings.set_parameter("mip_relative_gap", 0.01) # 1% optimality gap - -# Solve -problem.solve(settings) - -# Check status and extract results -status = problem.Status.name -print(f"Status: {status}") - -if status in ["Optimal", "FeasibleFound"]: - print(f"Total cost: ${problem.ObjValue:.2f}") - print("\nOpen warehouses:") - for w in warehouses: - if y[w].getValue() > 0.5: - print(f" {w} (fixed cost: ${fixed_costs[w]})") - - print("\nShipments:") - for w in warehouses: - for c in customers: - shipped = x[w, c].getValue() - if shipped > 0.01: - print(f" {w} -> {c}: {shipped:.1f} units") -else: - print(f"No solution found. Status: {status}") -``` - -## Python: Quadratic Programming (QP) - Beta - -```python -""" -Portfolio Optimization QP (more complex): - minimize x^T * Q * x (variance/risk) - subject to sum(x) = 1 (fully invested) - r^T * x >= target (minimum return) - x >= 0 (no short selling) -""" -from cuopt.linear_programming.problem import Problem, CONTINUOUS, MINIMIZE -from cuopt.linear_programming.solver_settings import SolverSettings - -# Create problem -problem = Problem("PortfolioOptimization") - -# Decision variables: portfolio weights for 3 assets -x1 = problem.addVariable(lb=0.0, ub=1.0, vtype=CONTINUOUS, name="stock_a") -x2 = problem.addVariable(lb=0.0, ub=1.0, vtype=CONTINUOUS, name="stock_b") -x3 = problem.addVariable(lb=0.0, ub=1.0, vtype=CONTINUOUS, name="stock_c") - -# Expected returns -r1, r2, r3 = 0.12, 0.08, 0.05 # 12%, 8%, 5% -target_return = 0.08 - -# Quadratic objective: minimize variance = x^T * Q * x -# Expand: 0.04*x1^2 + 0.02*x2^2 + 0.01*x3^2 + 2*0.01*x1*x2 + 2*0.005*x1*x3 + 2*0.008*x2*x3 -problem.setObjective( - 0.04 * x1 * x1 + 0.02 * x2 * x2 + 0.01 * x3 * x3 + - 0.02 * x1 * x2 + 0.01 * x1 * x3 + 0.016 * x2 * x3, - sense=MINIMIZE -) - -# Constraints -problem.addConstraint(x1 + x2 + x3 == 1, name="fully_invested") -problem.addConstraint(r1 * x1 + r2 * x2 + r3 * x3 >= target_return, name="min_return") - -# Solve -settings = SolverSettings() -settings.set_parameter("time_limit", 60) -problem.solve(settings) - -# Check status and extract results -status = problem.Status.name -print(f"Status: {status}") - -if status in ["Optimal", "PrimalFeasible"]: - print(f"Portfolio variance: {problem.ObjValue:.6f}") - print(f"Portfolio std dev: {problem.ObjValue**0.5:.4f}") - print(f"\nOptimal allocation:") - print(f" Stock A: {x1.getValue()*100:.2f}%") - print(f" Stock B: {x2.getValue()*100:.2f}%") - print(f" Stock C: {x3.getValue()*100:.2f}%") - exp_return = r1*x1.getValue() + r2*x2.getValue() + r3*x3.getValue() - print(f"\nExpected return: {exp_return*100:.2f}%") -else: - print(f"No optimal solution found. Status: {status}") -``` diff --git a/.github/agents/resources/cuopt-user/server_examples.md b/.github/agents/resources/cuopt-user/server_examples.md deleted file mode 100644 index 4cf4147ba6..0000000000 --- a/.github/agents/resources/cuopt-user/server_examples.md +++ /dev/null @@ -1,199 +0,0 @@ -# Server REST API examples (cuOpt) - -## Server: Start the Server - -```bash -# Start server in background -python3 -m cuopt_server.cuopt_service --ip 0.0.0.0 --port 8000 & -SERVER_PID=$! - -# Wait for server to be ready -sleep 5 -curl -fsS "http://localhost:8000/cuopt/health" -``` - -## Server: Routing Request (curl) - -```bash -# Submit a VRP request -REQID=$(curl -s --location "http://localhost:8000/cuopt/request" \ - --header 'Content-Type: application/json' \ - --header "CLIENT-VERSION: custom" \ - -d '{ - "cost_matrix_data": { - "data": { - "0": [ - [0, 10, 15, 20], - [10, 0, 12, 18], - [15, 12, 0, 10], - [20, 18, 10, 0] - ] - } - }, - "travel_time_matrix_data": { - "data": { - "0": [ - [0, 10, 15, 20], - [10, 0, 12, 18], - [15, 12, 0, 10], - [20, 18, 10, 0] - ] - } - }, - "task_data": { - "task_locations": [1, 2, 3], - "demand": [[10, 15, 20]], - "task_time_windows": [[0, 100], [10, 80], [20, 90]], - "service_times": [5, 5, 5] - }, - "fleet_data": { - "vehicle_locations": [[0, 0], [0, 0]], - "capacities": [[50, 50]], - "vehicle_time_windows": [[0, 200], [0, 200]] - }, - "solver_config": { - "time_limit": 5 - } - }' | jq -r '.reqId') - -echo "Request ID: $REQID" - -# Poll for solution -sleep 2 -curl -s --location "http://localhost:8000/cuopt/solution/${REQID}" \ - --header 'Content-Type: application/json' \ - --header "CLIENT-VERSION: custom" | jq . -``` - -## Server: Routing Request (Python with requests) - -```python -import requests -import time - -SERVER_URL = "http://localhost:8000" -HEADERS = { - "Content-Type": "application/json", - "CLIENT-VERSION": "custom" -} - -# VRP problem data -payload = { - "cost_matrix_data": { - "data": { - "0": [ - [0, 10, 15, 20, 25], - [10, 0, 12, 18, 22], - [15, 12, 0, 10, 15], - [20, 18, 10, 0, 8], - [25, 22, 15, 8, 0] - ] - } - }, - "travel_time_matrix_data": { - "data": { - "0": [ - [0, 10, 15, 20, 25], - [10, 0, 12, 18, 22], - [15, 12, 0, 10, 15], - [20, 18, 10, 0, 8], - [25, 22, 15, 8, 0] - ] - } - }, - "task_data": { - "task_locations": [1, 2, 3, 4], - "demand": [[20, 30, 25, 15]], - "task_time_windows": [[0, 50], [10, 60], [20, 70], [0, 80]], - "service_times": [5, 5, 5, 5] - }, - "fleet_data": { - "vehicle_locations": [[0, 0], [0, 0]], - "capacities": [[100, 100]], - "vehicle_time_windows": [[0, 200], [0, 200]] - }, - "solver_config": { - "time_limit": 10 - } -} - -# Submit request -response = requests.post( - f"{SERVER_URL}/cuopt/request", - json=payload, - headers=HEADERS -) -response.raise_for_status() -req_id = response.json()["reqId"] -print(f"Request submitted: {req_id}") - -# Poll for solution -max_attempts = 30 -for attempt in range(max_attempts): - response = requests.get( - f"{SERVER_URL}/cuopt/solution/{req_id}", - headers=HEADERS - ) - result = response.json() - - if "response" in result: - solver_response = result["response"].get("solver_response", {}) - print(f"\nSolution found!") - print(f"Status: {solver_response.get('status', 'N/A')}") - print(f"Cost: {solver_response.get('solution_cost', 'N/A')}") - - if "vehicle_data" in solver_response: - for vid, vdata in solver_response["vehicle_data"].items(): - route = vdata.get("route", []) - print(f"Vehicle {vid}: {' -> '.join(map(str, route))}") - break - else: - print(f"Waiting... (attempt {attempt + 1})") - time.sleep(1) -``` - -## Server: LP/MILP Request - -```bash -# Submit LP problem via REST -# Production Planning: maximize 40*chairs + 30*tables -# subject to: 2*chairs + 3*tables <= 240 (wood) -# 4*chairs + 2*tables <= 200 (labor) -# chairs, tables >= 0 -REQID=$(curl -s --location "http://localhost:8000/cuopt/request" \ - --header 'Content-Type: application/json' \ - --header "CLIENT-VERSION: custom" \ - -d '{ - "csr_constraint_matrix": { - "offsets": [0, 2, 4], - "indices": [0, 1, 0, 1], - "values": [2.0, 3.0, 4.0, 2.0] - }, - "constraint_bounds": { - "upper_bounds": [240.0, 200.0], - "lower_bounds": ["ninf", "ninf"] - }, - "objective_data": { - "coefficients": [40.0, 30.0], - "scalability_factor": 1.0, - "offset": 0.0 - }, - "variable_bounds": { - "upper_bounds": ["inf", "inf"], - "lower_bounds": [0.0, 0.0] - }, - "maximize": true, - "solver_config": { - "tolerances": {"optimality": 0.0001}, - "time_limit": 60 - } - }' | jq -r '.reqId') - -echo "Request ID: $REQID" - -# Get solution -sleep 2 -curl -s --location "http://localhost:8000/cuopt/solution/${REQID}" \ - --header 'Content-Type: application/json' \ - --header "CLIENT-VERSION: custom" | jq . -``` diff --git a/.github/skills/README.md b/.github/skills/README.md new file mode 100644 index 0000000000..6ffdb6c5e7 --- /dev/null +++ b/.github/skills/README.md @@ -0,0 +1,40 @@ +# cuOpt Skills + +This directory contains AI agent skills for NVIDIA cuOpt. + +## For Agents + +1. **First**: Read `cuopt-user-rules/SKILL.md` for user tasks +2. **Then**: Read the relevant domain skill + +For development tasks, read `cuopt-developer/SKILL.md` (has its own rules). + +## Skills Index + +### Rules +| Skill | Description | +|-------|-------------| +| `cuopt-user-rules/` | Behavior rules for user tasks (read first) | + +### Problem-Solving +| Skill | Description | +|-------|-------------| +| `cuopt-routing/` | VRP, TSP, PDP, fleet optimization | +| `cuopt-lp-milp/` | Linear & mixed-integer programming | +| `cuopt-qp/` | Quadratic programming (beta) | + +### Workflow +| Skill | Description | +|-------|-------------| +| `cuopt-debugging/` | Troubleshooting, errors, diagnostics | +| `cuopt-installation/` | Setup, pip, conda, Docker, GPU | + +### Integration +| Skill | Description | +|-------|-------------| +| `cuopt-server/` | REST API deployment | + +### Development +| Skill | Description | +|-------|-------------| +| `cuopt-developer/` | Contributing to codebase (own rules) | diff --git a/.github/skills/cuopt-debugging/SKILL.md b/.github/skills/cuopt-debugging/SKILL.md new file mode 100644 index 0000000000..649b06387d --- /dev/null +++ b/.github/skills/cuopt-debugging/SKILL.md @@ -0,0 +1,267 @@ +--- +name: cuopt-debugging +description: Troubleshoot cuOpt problems including errors, wrong results, infeasible solutions, performance issues, and status codes. Use when the user says something isn't working, gets unexpected results, or needs help diagnosing issues. +--- + +# cuOpt Debugging Skill + +> **Prerequisites**: Read `cuopt-user-rules/SKILL.md` first for behavior rules. + +Diagnose and fix issues with cuOpt solutions, errors, and performance. + +## Before You Start: Required Questions + +**Ask these to understand the problem:** + +1. **What's the symptom?** + - Error message? + - Wrong/unexpected results? + - Empty solution? + - Performance too slow? + +2. **What's the status?** + - For LP/MILP: `problem.Status.name` + - For Routing: `solution.get_status()` + - For Server: HTTP response code + +3. **Can you share?** + - The error message (exact text) + - The code that produces it + - Problem size (variables, constraints, locations) + - Share important log messages or status of on-going run. + +## Quick Diagnosis by Symptom + +### "Solution is empty/None but status looks OK" + +**Most common cause: Wrong status string case** + +```python +# ❌ WRONG - "OPTIMAL" never matches, silently fails +if problem.Status.name == "OPTIMAL": + print(problem.ObjValue) # Never runs! + +# ✅ CORRECT - use PascalCase +if problem.Status.name in ["Optimal", "FeasibleFound"]: + print(problem.ObjValue) +``` + +**Diagnostic code:** +```python +print(f"Actual status: '{problem.Status.name}'") +print(f"Matches 'Optimal': {problem.Status.name == 'Optimal'}") +print(f"Matches 'OPTIMAL': {problem.Status.name == 'OPTIMAL'}") +``` + +### "Objective value is wrong/zero" + +**Check if variables are actually used:** +```python +for var in [x, y, z]: + print(f"{var.name}: {var.getValue()}") +print(f"Objective: {problem.ObjValue}") +``` + +**Common causes:** +- Constraints too restrictive (all zeros is feasible) +- Objective coefficients have wrong sign +- Wrong variable in objective + +### "Infeasible" status + +**For LP/MILP:** +```python +if problem.Status.name == "Infeasible": + print("Problem has no feasible solution") + # Check constraints manually + for name in constraint_names: + c = problem.getConstraint(name) + print(f"{name}: {c}") +``` + +**Common causes:** +- Conflicting constraints (x <= 5 AND x >= 10) +- Bounds too tight +- Missing a "slack" variable for soft constraints + +**For Routing:** +```python +if solution.get_status() != 0: + print(f"Error: {solution.get_error_message()}") + infeasible = solution.get_infeasible_orders() + print(f"Infeasible orders: {infeasible.to_list()}") +``` + +**Common routing infeasibility causes:** +- Time windows too tight (earliest > vehicle latest) +- Total demand > total capacity +- Order location unreachable in time + +### "Integer variable has fractional value" + +```python +# Check how variable was defined +int_var = problem.addVariable( + lb=0, ub=10, + vtype=INTEGER, # Must be INTEGER, not CONTINUOUS + name="count" +) + +# Also check if status is actually optimal +if problem.Status.name == "FeasibleFound": + print("Warning: not fully optimal, may have fractional intermediate values") +``` + +### Server returns 422 Validation Error + +**Check payload against OpenAPI spec:** + +Common field name mistakes: +``` +❌ transit_time_matrix_data → ✅ travel_time_matrix_data +❌ vehicle_capacities → ✅ capacities +❌ locations → ✅ task_locations +``` + +**Capacity format:** +```json +// ❌ WRONG +"capacities": [[50], [50]] + +// ✅ CORRECT +"capacities": [[50, 50]] +``` + +### OutOfMemoryError + +**Check problem size:** +```python +print(f"Variables: {problem.num_variables}") +print(f"Constraints: {problem.num_constraints}") + +# For routing +print(f"Locations: {n_locations}") +print(f"Orders: {n_orders}") +print(f"Fleet: {n_fleet}") +``` + +**Mitigations:** +- Reduce problem size +- Use sparse constraint matrix +- For routing: reduce time limit, simplify constraints + +### cudf Type Errors + +**Always use explicit dtypes:** +```python +cost_matrix = cost_matrix.astype("float32") +demand = cudf.Series([...], dtype="int32") +order_locations = cudf.Series([...], dtype="int32") +time_windows = cudf.Series([...], dtype="int32") +``` + +### MPS Parsing Fails + +**Check MPS format:** +```bash +head -30 problem.mps +``` + +**Required sections in order:** +1. NAME +2. ROWS +3. COLUMNS +4. RHS +5. (optional) BOUNDS +6. ENDATA + +**Common issues:** +- Missing ENDATA +- Integer markers malformed: `'MARKER'`, `'INTORG'`, `'INTEND'` +- Invalid characters or encoding + +## Status Code Reference + +### LP Status Values +| Status | Meaning | +|--------|---------| +| `Optimal` | Found optimal solution | +| `PrimalFeasible` | Found feasible but may not be optimal | +| `PrimalInfeasible` | No feasible solution exists | +| `DualInfeasible` | Problem is unbounded | +| `TimeLimit` | Stopped due to time limit | +| `IterationLimit` | Stopped due to iteration limit | +| `NumericalError` | Numerical issues encountered | +| `NoTermination` | Solver didn't converge | + +### MILP Status Values +| Status | Meaning | +|--------|---------| +| `Optimal` | Found optimal solution | +| `FeasibleFound` | Found feasible, within gap tolerance | +| `Infeasible` | No feasible solution exists | +| `Unbounded` | Problem is unbounded | +| `TimeLimit` | Stopped due to time limit | +| `NoTermination` | No solution found yet | + +### Routing Status Values +| Code | Meaning | +|------|---------| +| 0 | SUCCESS | +| 1 | FAIL | +| 2 | TIMEOUT | +| 3 | EMPTY | + +## Performance Debugging + +### Slow LP/MILP Solve + +```python +settings = SolverSettings() +settings.set_parameter("log_to_console", 1) # See progress +settings.set_parameter("time_limit", 60) # Don't wait forever + +# For MILP, accept good-enough solution +settings.set_parameter("mip_relative_gap", 0.05) # 5% gap +``` + +### Slow Routing Solve + +```python +ss = routing.SolverSettings() +ss.set_time_limit(60) # Increase time for better solutions +ss.set_verbose_mode(True) # See progress during solve +``` + +## Diagnostic Checklist + +``` +□ Status checked with correct case (PascalCase)? +□ All variables have correct vtype (INTEGER vs CONTINUOUS)? +□ Constraint directions correct (<= vs >= vs ==)? +□ Objective sense correct (MINIMIZE vs MAXIMIZE)? +□ For QP: using MINIMIZE (not MAXIMIZE)? +□ Data types explicit (float32, int32)? +□ Matrix dimensions match n_locations? +□ Time windows have transit_time_matrix? +``` + +## Diagnostic Code Snippets + +See [resources/diagnostic_snippets.md](resources/diagnostic_snippets.md) for copy-paste diagnostic code: +- Status checking +- Variable inspection +- Constraint analysis +- Routing infeasibility diagnosis +- Server response debugging +- Memory and performance checks + +## When to Escalate + +Switch to **cuopt-developer** if: +- Bug appears to be in cuOpt itself +- Need to examine solver internals + +File a GitHub issue if: +- Reproducible bug with minimal example +- Include: cuOpt version, CUDA version, error message, minimal repro code diff --git a/.github/skills/cuopt-debugging/resources/diagnostic_snippets.md b/.github/skills/cuopt-debugging/resources/diagnostic_snippets.md new file mode 100644 index 0000000000..61f5c19bfd --- /dev/null +++ b/.github/skills/cuopt-debugging/resources/diagnostic_snippets.md @@ -0,0 +1,219 @@ +# Debugging: Diagnostic Snippets + +## LP/MILP Diagnostics + +### Check Status Properly + +```python +# Print actual status value +print(f"Status: '{problem.Status.name}'") +print(f"Status type: {type(problem.Status.name)}") + +# Common mistake: wrong case +print(f"== 'Optimal': {problem.Status.name == 'Optimal'}") # ✅ +print(f"== 'OPTIMAL': {problem.Status.name == 'OPTIMAL'}") # ❌ Always False +``` + +### Inspect Variables + +```python +# Check all variable values +for var in [x, y, z]: + print(f"{var.name}: lb={var.lb}, ub={var.ub}, value={var.getValue()}") + +# Check if integer variables are actually integer +for var in integer_vars: + val = var.getValue() + is_int = abs(val - round(val)) < 1e-6 + print(f"{var.name}: {val} (is_integer: {is_int})") +``` + +### Inspect Constraints + +```python +# Check constraint values +for name in ["constraint1", "constraint2"]: + c = problem.getConstraint(name) + print(f"{name}: dual={c.DualValue}") +``` + +### Check Problem Size + +```python +print(f"Variables: {problem.num_variables}") +print(f"Constraints: {problem.num_constraints}") +``` + +## Routing Diagnostics + +### Check Solution Status + +```python +status = solution.get_status() +print(f"Status code: {status}") +# 0 = SUCCESS +# 1 = FAIL +# 2 = TIMEOUT +# 3 = EMPTY + +if status != 0: + print(f"Message: {solution.get_message()}") + print(f"Error: {solution.get_error_message()}") +``` + +### Find Infeasible Orders + +```python +infeasible = solution.get_infeasible_orders() +if len(infeasible) > 0: + print(f"Infeasible orders: {infeasible.to_list()}") + + # Check why each is infeasible + for order_idx in infeasible.to_list(): + print(f"\nOrder {order_idx}:") + print(f" Location: {order_locations[order_idx]}") + print(f" Time window: [{order_earliest[order_idx]}, {order_latest[order_idx]}]") + print(f" Demand: {demand[order_idx]}") +``` + +### Verify Data Dimensions + +```python +print(f"Cost matrix shape: {cost_matrix.shape}") +print(f"n_locations declared: {dm.n_locations}") +print(f"n_orders: {len(order_locations)}") +print(f"n_fleet: {dm.n_fleet}") + +# Check consistency +assert cost_matrix.shape[0] == cost_matrix.shape[1], "Matrix not square" +assert cost_matrix.shape[0] == dm.n_locations, "Matrix size != n_locations" +``` + +### Check Data Types + +```python +# For numpy arrays, use .dtype directly +# For pandas/cudf DataFrames, use .values.dtype or .to_numpy().dtype +print(f"cost_matrix dtype: {cost_matrix.values.dtype}") # float32 +print(f"order_locations dtype: {order_locations.values.dtype}") # int32 +print(f"demand dtype: {demand.values.dtype}") # int32 +``` + +### Verify Time Windows Feasibility + +```python +# Check for impossible time windows +for i in range(len(order_earliest)): + if order_earliest[i] > order_latest[i]: + print(f"Order {i}: earliest ({order_earliest[i]}) > latest ({order_latest[i]})") + +# Check if orders are reachable from depot in time +depot = 0 +for i in range(len(order_locations)): + loc = order_locations[i] + travel_time = transit_time_matrix.iloc[depot, loc] + if travel_time > order_latest[i]: + print(f"Order {i}: unreachable (travel={travel_time}, latest={order_latest[i]})") +``` + +### Check Capacity Feasibility + +```python +total_demand = demand.sum() +total_capacity = vehicle_capacity.sum() +print(f"Total demand: {total_demand}") +print(f"Total capacity: {total_capacity}") +if total_demand > total_capacity: + print("WARNING: Total demand exceeds total capacity!") +``` + +## Server Diagnostics + +### Check Response Structure + +```python +import json + +response = requests.get(f"{SERVER}/cuopt/solution/{req_id}", headers=HEADERS) +print(f"Status code: {response.status_code}") +print(f"Response: {json.dumps(response.json(), indent=2)}") +``` + +### Validate Payload Against Schema + +```bash +# Get OpenAPI spec +curl -s http://localhost:8000/cuopt.yaml > cuopt_spec.yaml + +# Check payload structure manually +``` + +### Common 422 Error Fixes + +```python +# ❌ Wrong field name +payload = {"transit_time_matrix_data": {...}} + +# ✅ Correct field name +payload = {"travel_time_matrix_data": {...}} + +# ❌ Wrong capacity format +"capacities": [[50], [50]] + +# ✅ Correct capacity format +"capacities": [[50, 50]] +``` + +## Memory Diagnostics + +### Check GPU Memory + +```python +import subprocess +result = subprocess.run(['nvidia-smi'], capture_output=True, text=True) +print(result.stdout) +``` + +### Estimate Problem Memory + +```python +# Rough estimate for routing +n_locations = 1000 +n_fleet = 50 +n_orders = 500 + +# Cost matrix: n_locations² * 4 bytes (float32) +matrix_size = n_locations * n_locations * 4 / 1e9 # GB +print(f"Cost matrix: ~{matrix_size:.2f} GB") +``` + +## Performance Diagnostics + +### Time the Solve + +```python +import time + +start = time.time() +problem.solve(settings) +elapsed = time.time() - start +print(f"Solve time: {elapsed:.2f}s") +``` + +### Enable Solver Logging + +```python +settings = SolverSettings() +settings.set_parameter("log_to_console", 1) +``` + +--- + +## Additional References + +| Topic | Resource | +|-------|----------| +| Python API docstrings | `python/cuopt/cuopt/routing/vehicle_routing.py` | +| LP/MILP Problem class | `python/cuopt/cuopt/linear_programming/problem.py` | +| Server API spec | `docs/cuopt/source/cuopt_spec.yaml` | +| Troubleshooting guide | [NVIDIA cuOpt Docs](https://docs.nvidia.com/cuopt/user-guide/latest/troubleshooting.html) | diff --git a/.github/skills/cuopt-developer/SKILL.md b/.github/skills/cuopt-developer/SKILL.md new file mode 100644 index 0000000000..8e73995b58 --- /dev/null +++ b/.github/skills/cuopt-developer/SKILL.md @@ -0,0 +1,310 @@ +--- +name: cuopt-developer +description: Contribute to NVIDIA cuOpt codebase including C++/CUDA, Python, server, docs, and CI. Use when the user wants to modify solver internals, add features, submit PRs, or understand the codebase architecture. +--- + +# cuOpt Developer Skill + +Contribute to the NVIDIA cuOpt codebase. This skill is for modifying cuOpt itself, not for using it. + +**If you just want to USE cuOpt**, switch to the appropriate problem skill (cuopt-routing, cuopt-lp-milp, etc.) + +--- + +## Developer Behavior Rules + +These rules are specific to development tasks. They differ from user rules. + +### 1. Ask Before Assuming + +Clarify before implementing: +- What component? (C++/CUDA, Python, server, docs, CI) +- What's the goal? (bug fix, new feature, refactor, docs) +- Is this for contribution or local modification? + +### 2. Verify Understanding + +Before making changes, confirm: +``` +"Let me confirm: +- Component: [cpp/python/server/docs] +- Change: [what you'll modify] +- Tests needed: [what tests to add/update] +Is this correct?" +``` + +### 3. Follow Codebase Patterns + +- Read existing code in the area you're modifying +- Match naming conventions, style, and patterns +- Don't invent new patterns without discussion + +### 4. Ask Before Running — Modified for Dev + +**OK to run without asking** (expected for dev work): +- `./build.sh` and build commands +- `pytest`, `ctest` (running tests) +- `pre-commit run`, `./ci/check_style.sh` (formatting) +- `git status`, `git diff`, `git log` (read-only git) + +**Still ask before**: +- `git commit`, `git push` (write operations) +- Package installs (`pip`, `conda`, `apt`) +- Any destructive or irreversible commands + +### 5. No Privileged Operations + +Same as user rules — never without explicit request: +- No `sudo` +- No system file changes +- No writes outside workspace + +--- + +## Before You Start: Required Questions + +**Ask these if not already clear:** + +1. **What are you trying to change?** + - Solver algorithm/performance? + - Python API? + - Server endpoints? + - Documentation? + - CI/build system? + +2. **Do you have the development environment set up?** + - Built the project successfully? + - Ran tests? + +3. **Is this for contribution or local modification?** + - If contributing: will need to follow DCO signoff + +## Project Architecture + +``` +cuopt/ +├── cpp/ # Core C++ engine +│ ├── include/cuopt/ # Public C/C++ headers +│ ├── src/ # Implementation (CUDA kernels) +│ └── tests/ # C++ unit tests (gtest) +├── python/ +│ ├── cuopt/ # Python bindings and routing API +│ ├── cuopt_server/ # REST API server +│ ├── cuopt_self_hosted/ # Self-hosted deployment +│ └── libcuopt/ # Python wrapper for C library +├── ci/ # CI/CD scripts +├── docs/ # Documentation source +└── datasets/ # Test datasets +``` + +## Supported APIs + +| API Type | LP | MILP | QP | Routing | +|----------|:--:|:----:|:--:|:-------:| +| C API | ✓ | ✓ | ✓ | ✗ | +| C++ API | (internal) | (internal) | (internal) | (internal) | +| Python | ✓ | ✓ | ✓ | ✓ | +| Server | ✓ | ✓ | ✗ | ✓ | + +## Safety Rules (Non-Negotiable) + +### Minimal Diffs +- Change only what's necessary +- Avoid drive-by refactors +- No mass reformatting of unrelated code + +### No API Invention +- Don't invent new APIs without discussion +- Align with existing patterns in `docs/cuopt/source/` +- Server schemas must match OpenAPI spec + +### Don't Bypass CI +- Never suggest `--no-verify` or skipping checks +- All PRs must pass CI + +### CUDA/GPU Hygiene +- Keep operations stream-ordered +- Follow existing RAFT/RMM patterns +- No raw `new`/`delete` - use RMM allocators + +## Build & Test + +### Build Everything + +```bash +./build.sh +``` + +### Build Specific Components + +```bash +./build.sh libcuopt # C++ library +./build.sh cuopt # Python package +./build.sh cuopt_server # Server +./build.sh docs # Documentation +``` + +### Run Tests + +```bash +# C++ tests +ctest --test-dir cpp/build + +# Python tests +pytest -v python/cuopt/cuopt/tests + +# Server tests +pytest -v python/cuopt_server/tests +``` + +## Before You Commit + +### Run Style Checks + +```bash +./ci/check_style.sh +# or +pre-commit run --all-files --show-diff-on-failure +``` + +### Sign Your Commits (DCO Required) + +```bash +git commit -s -m "Your message" +``` + +## Coding Conventions + +### C++ Naming + +| Element | Convention | Example | +|---------|------------|---------| +| Variables | `snake_case` | `num_locations` | +| Functions | `snake_case` | `solve_problem()` | +| Classes | `snake_case` | `data_model` | +| Test cases | `PascalCase` | `SolverTest` | +| Device data | `d_` prefix | `d_locations_` | +| Host data | `h_` prefix | `h_data_` | +| Template params | `_t` suffix | `value_t` | +| Private members | `_` suffix | `n_locations_` | + +### File Extensions + +| Extension | Usage | +|-----------|-------| +| `.hpp` | C++ headers | +| `.cpp` | C++ source | +| `.cu` | CUDA source (nvcc required) | +| `.cuh` | CUDA headers with device code | + +### Include Order + +1. Local headers +2. RAPIDS headers +3. Related libraries +4. Dependencies +5. STL + +### Python Style + +- Follow PEP 8 +- Use type hints +- Tests use pytest + +## Error Handling + +### Runtime Assertions + +```cpp +CUOPT_EXPECTS(condition, "Error message"); +CUOPT_FAIL("Unreachable code reached"); +``` + +### CUDA Error Checking + +```cpp +RAFT_CUDA_TRY(cudaMemcpy(...)); +``` + +## Memory Management + +```cpp +// ❌ WRONG +int* data = new int[100]; + +// ✅ CORRECT - use RMM +rmm::device_uvector data(100, stream); +``` + +- All operations should accept `cuda_stream_view` +- Views (`*_view` suffix) are non-owning + +## Test Impact Check + +**Before any behavioral change, ask:** + +1. What scenarios must be covered? +2. What's the expected behavior contract? +3. Where should tests live? + - C++ gtests: `cpp/tests/` + - Python pytest: `python/.../tests/` + +**Add at least one regression test for new behavior.** + +## Key Files Reference + +| Purpose | Location | +|---------|----------| +| Main build script | `build.sh` | +| Dependencies | `dependencies.yaml` | +| C++ formatting | `.clang-format` | +| Conda environments | `conda/environments/` | +| Test data | `datasets/` | +| CI scripts | `ci/` | + +## Common Tasks + +### Adding a Solver Parameter + +1. Add to settings struct in `cpp/include/cuopt/` +2. Expose in Python bindings `python/cuopt/` +3. Add to server schema if applicable +4. Add tests +5. Update documentation + +### Adding a Server Endpoint + +1. Add route in `python/cuopt_server/cuopt_server/webserver.py` +2. Update OpenAPI spec `docs/cuopt/source/cuopt_spec.yaml` +3. Add tests in `python/cuopt_server/tests/` +4. Update documentation + +### Modifying CUDA Kernels + +1. Edit kernel in `cpp/src/` +2. Follow stream-ordering patterns +3. Run C++ tests: `ctest --test-dir cpp/build` +4. Run benchmarks to check performance + +## Common Pitfalls + +| Problem | Solution | +|---------|----------| +| Cython changes not reflected | Rerun: `./build.sh cuopt` | +| Missing `nvcc` | Set `$CUDACXX` or add CUDA to `$PATH` | +| CUDA out of memory | Reduce problem size | +| Slow debug library loading | Device symbols cause delay | + +## Canonical Documentation + +- **Contributing/build/test**: `CONTRIBUTING.md` +- **CI scripts**: `ci/README.md` +- **Release scripts**: `ci/release/README.md` +- **Docs build**: `docs/cuopt/README.md` + +## Security Rules + +- **No shell commands by default** - provide instructions, only run if asked +- **No package installs by default** - ask before pip/conda/apt +- **No privileged changes** - never use sudo without explicit request +- **Workspace-only file changes** - ask for permission for writes outside repo diff --git a/.github/skills/cuopt-installation/SKILL.md b/.github/skills/cuopt-installation/SKILL.md new file mode 100644 index 0000000000..981063f477 --- /dev/null +++ b/.github/skills/cuopt-installation/SKILL.md @@ -0,0 +1,295 @@ +--- +name: cuopt-installation +description: Install and set up NVIDIA cuOpt including pip, conda, Docker, and GPU requirements. Use when the user asks about installation, setup, environments, CUDA versions, GPU requirements, or getting started. +--- + +# cuOpt Installation Skill + +> **Prerequisites**: Read `cuopt-user-rules/SKILL.md` first for behavior rules. + +Set up NVIDIA cuOpt for GPU-accelerated optimization. + +## Before You Start: Required Questions + +**Ask these if not already clear:** + +1. **What's your environment?** + - Local machine with NVIDIA GPU? + - Cloud instance (AWS, GCP, Azure)? + - Docker/Kubernetes? + - No GPU (need cloud solution)? + +2. **What's your CUDA version?** + ```bash + nvcc --version + # or + nvidia-smi + ``` + +3. **What do you need?** + - Python API only? + - REST Server for production? + - C API for embedding? + +4. **Package manager preference?** + - pip + - conda + - Docker + +## System Requirements + +### GPU Requirements +- NVIDIA GPU with Compute Capability >= 7.0 (Volta or newer) +- Supported: V100, A100, H100, RTX 20xx/30xx/40xx, etc. +- NOT supported: GTX 10xx series (Pascal) + +### CUDA Requirements +- CUDA 12.x or CUDA 13.x (match package suffix) +- Compatible NVIDIA driver + +### Check Your System + +```bash +# Check GPU +nvidia-smi + +# Check CUDA version +nvcc --version + +# Check compute capability +nvidia-smi --query-gpu=compute_cap --format=csv +``` + +## Installation Methods + +### pip (Recommended for Python) + +```bash +# For CUDA 13 +pip install --extra-index-url=https://pypi.nvidia.com cuopt-cu13 + +# For CUDA 12 +pip install --extra-index-url=https://pypi.nvidia.com cuopt-cu12 + +# With version pinning (recommended for reproducibility) +pip install --extra-index-url=https://pypi.nvidia.com 'cuopt-cu12==26.2.*' +``` + +### pip: Server + Client + +```bash +# CUDA 12 example +pip install --extra-index-url=https://pypi.nvidia.com \ + cuopt-server-cu12 cuopt-sh-client + +# With version pinning +pip install --extra-index-url=https://pypi.nvidia.com \ + cuopt-server-cu12==26.02.* cuopt-sh-client==26.02.* +``` + +### conda + +```bash +# Python API +conda install -c rapidsai -c conda-forge -c nvidia cuopt + +# Server + client +conda install -c rapidsai -c conda-forge -c nvidia cuopt-server cuopt-sh-client + +# With version pinning +conda install -c rapidsai -c conda-forge -c nvidia cuopt=26.02.* +``` + +### Docker (Recommended for Server) + +```bash +# Pull image +docker pull nvidia/cuopt:latest-cuda12.9-py3.13 + +# Run server +docker run --gpus all -it --rm \ + -p 8000:8000 \ + -e CUOPT_SERVER_PORT=8000 \ + nvidia/cuopt:latest-cuda12.9-py3.13 + +# Verify +curl http://localhost:8000/cuopt/health +``` + +### Docker: Interactive Python + +```bash +docker run --gpus all -it --rm nvidia/cuopt:latest-cuda12.9-py3.13 python +``` + +## Verification + +### Verify Python Installation + +```python +# Test import +import cuopt +print(f"cuOpt version: {cuopt.__version__}") + +# Test GPU access +from cuopt import routing +dm = routing.DataModel(n_locations=3, n_fleet=1, n_orders=2) +print("GPU access OK") +``` + +### Verify Server Installation + +```bash +# Start server +python -m cuopt_server.cuopt_service --ip 0.0.0.0 --port 8000 & + +# Wait and test +sleep 5 +curl -s http://localhost:8000/cuopt/health | jq . +``` + +### Verify C API Installation + +```bash +# Find header +find $CONDA_PREFIX -name "cuopt_c.h" + +# Find library +find $CONDA_PREFIX -name "libcuopt.so" +``` + +## Common Installation Issues + +### "No module named 'cuopt'" + +```bash +# Check if installed +pip list | grep cuopt + +# Check Python environment +which python +echo $CONDA_PREFIX + +# Reinstall +pip uninstall cuopt-cu12 cuopt-cu13 +pip install --extra-index-url=https://pypi.nvidia.com cuopt-cu12 +``` + +### "CUDA not available" / GPU not detected + +```bash +# Check NVIDIA driver +nvidia-smi + +# Check CUDA toolkit +nvcc --version + +# In Python +import torch # if using PyTorch +print(torch.cuda.is_available()) +``` + +### Version mismatch (CUDA 12 vs 13) + +```bash +# Check installed CUDA +nvcc --version + +# Install matching package +# For CUDA 12.x +pip install cuopt-cu12 + +# For CUDA 13.x +pip install cuopt-cu13 +``` + +### Docker: "could not select device driver" + +```bash +# Install NVIDIA Container Toolkit +# Ubuntu: +distribution=$(. /etc/os-release;echo $ID$VERSION_ID) +curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add - +curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | \ + sudo tee /etc/apt/sources.list.d/nvidia-docker.list +sudo apt-get update +sudo apt-get install -y nvidia-container-toolkit +sudo systemctl restart docker +``` + +## Environment Setup + +### Create Clean Environment (conda) + +```bash +conda create -n cuopt-env python=3.11 +conda activate cuopt-env +conda install -c rapidsai -c conda-forge -c nvidia cuopt +``` + +### Create Clean Environment (pip/venv) + +```bash +python -m venv cuopt-env +source cuopt-env/bin/activate # Linux/Mac +pip install --extra-index-url=https://pypi.nvidia.com cuopt-cu12 +``` + +## Cloud Deployment + +### AWS + +- Use p4d.24xlarge (A100) or p3.2xlarge (V100) +- Deep Learning AMI has CUDA pre-installed +- Or use provided Docker image + +### GCP + +- Use a2-highgpu-1g (A100) or n1-standard with T4 +- Deep Learning VM has CUDA pre-installed + +### Azure + +- Use NC-series (T4, A100) +- Data Science VM has CUDA pre-installed + +## Offline Installation + +```bash +# Download wheels on connected machine +pip download --extra-index-url=https://pypi.nvidia.com cuopt-cu12 -d ./wheels + +# Transfer wheels directory to offline machine + +# Install from local wheels +pip install --no-index --find-links=./wheels cuopt-cu12 +``` + +## Upgrade + +```bash +# pip +pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12 + +# conda +conda update -c rapidsai -c conda-forge -c nvidia cuopt + +# Docker +docker pull nvidia/cuopt:latest-cuda12.9-py3.13 +``` + +## Verification Examples + +See [resources/verification_examples.md](resources/verification_examples.md) for: +- Python installation verification +- LP/MILP verification +- Server verification +- C API verification +- System requirements check +- Docker verification + +## Additional Resources + +- Full installation docs: `docs/cuopt/source/cuopt-python/quick-start.rst` +- Server setup: `docs/cuopt/source/cuopt-server/quick-start.rst` +- [NVIDIA cuOpt Documentation](https://docs.nvidia.com/cuopt/user-guide/latest/) diff --git a/.github/skills/cuopt-installation/resources/verification_examples.md b/.github/skills/cuopt-installation/resources/verification_examples.md new file mode 100644 index 0000000000..bd84de80ba --- /dev/null +++ b/.github/skills/cuopt-installation/resources/verification_examples.md @@ -0,0 +1,172 @@ +# Installation: Verification Examples + +## Verify Python Installation + +```python +# Basic import test +import cuopt +print(f"cuOpt version: {cuopt.__version__}") + +# GPU access test +from cuopt import routing + +dm = routing.DataModel(n_locations=3, n_fleet=1, n_orders=2) +print("DataModel created - GPU access OK") + +# Quick solve test +import cudf +cost_matrix = cudf.DataFrame([[0,1,2],[1,0,1],[2,1,0]], dtype="float32") +dm.add_cost_matrix(cost_matrix) +dm.set_order_locations(cudf.Series([1, 2])) + +solution = routing.Solve(dm, routing.SolverSettings()) +print(f"Solve status: {solution.get_status()}") +print("cuOpt installation verified!") +``` + +## Verify LP/MILP + +```python +from cuopt.linear_programming.problem import Problem, CONTINUOUS, MAXIMIZE +from cuopt.linear_programming.solver_settings import SolverSettings + +problem = Problem("Test") +x = problem.addVariable(lb=0, vtype=CONTINUOUS, name="x") +problem.setObjective(x, sense=MAXIMIZE) +problem.addConstraint(x <= 10) + +problem.solve(SolverSettings()) +print(f"Status: {problem.Status.name}") +print(f"x = {x.getValue()}") +print("LP/MILP working!") +``` + +## Verify Server Installation + +```bash +# Start server in background +python -m cuopt_server.cuopt_service --ip 0.0.0.0 --port 8000 & +SERVER_PID=$! + +# Wait for startup +sleep 5 + +# Health check +curl -s http://localhost:8000/cuopt/health | jq . + +# Quick routing test +curl -s -X POST "http://localhost:8000/cuopt/request" \ + -H "Content-Type: application/json" \ + -H "CLIENT-VERSION: custom" \ + -d '{ + "cost_matrix_data": {"data": {"0": [[0,1],[1,0]]}}, + "travel_time_matrix_data": {"data": {"0": [[0,1],[1,0]]}}, + "task_data": {"task_locations": [1]}, + "fleet_data": {"vehicle_locations": [[0,0]], "capacities": [[10]]}, + "solver_config": {"time_limit": 1} + }' | jq . + +# Stop server +kill $SERVER_PID +``` + +## Verify C API Installation + +```bash +# Find header +echo "Looking for cuopt_c.h..." +find ${CONDA_PREFIX:-/usr} -name "cuopt_c.h" 2>/dev/null + +# Find library +echo "Looking for libcuopt.so..." +find ${CONDA_PREFIX:-/usr} -name "libcuopt.so" 2>/dev/null + +# Test compile (if gcc available) +cat > /tmp/test_cuopt.c << 'EOF' +#include +#include +int main() { + printf("cuopt_c.h found and compilable\n"); + return 0; +} +EOF + +gcc -I${CONDA_PREFIX}/include -c /tmp/test_cuopt.c -o /tmp/test_cuopt.o && \ + echo "C API headers OK" || echo "C API headers not found" +``` + +## Check System Requirements + +```bash +# GPU check +nvidia-smi + +# CUDA version +nvcc --version + +# Compute capability (need >= 7.0) +nvidia-smi --query-gpu=compute_cap --format=csv,noheader + +# Python version +python --version + +# Available memory +nvidia-smi --query-gpu=memory.total,memory.free --format=csv +``` + +## Check Package Versions + +```python +import importlib.metadata + +packages = ["cuopt-cu12", "cuopt-cu13", "cuopt-server-cu12", "cuopt-server-cu13", "cuopt-sh-client"] +for pkg in packages: + try: + version = importlib.metadata.version(pkg) + print(f"{pkg}: {version}") + except importlib.metadata.PackageNotFoundError: + pass +``` + +## Troubleshooting Commands + +```bash +# Check if cuopt is installed +pip list | grep -i cuopt + +# Check conda packages +conda list | grep -i cuopt + +# Check CUDA runtime +python -c "import torch; print(torch.cuda.is_available())" 2>/dev/null || echo "PyTorch not installed" + +# Check cudf (routing dependency) +python -c "import cudf; print(f'cudf: {cudf.__version__}')" + +# Check rmm (memory manager) +python -c "import rmm; print(f'rmm: {rmm.__version__}')" +``` + +## Docker Verification + +```bash +# Pull and run +docker run --gpus all --rm nvidia/cuopt:latest-cuda12.9-py3.13 python -c " +import cuopt +print(f'cuOpt version: {cuopt.__version__}') +from cuopt import routing +dm = routing.DataModel(n_locations=3, n_fleet=1, n_orders=2) +print('GPU access OK') +" +``` + +--- + +## Additional References + +| Topic | Resource | +|-------|----------| +| Installation Guide | [NVIDIA cuOpt Docs](https://docs.nvidia.com/cuopt/user-guide/latest/installation.html) | +| System Requirements | [cuOpt Requirements](https://docs.nvidia.com/cuopt/user-guide/latest/requirements.html) | +| Docker Images | See `ci/docker/` in this repo | +| Conda Recipes | See `conda/recipes/` in this repo | diff --git a/.github/skills/cuopt-lp-milp/SKILL.md b/.github/skills/cuopt-lp-milp/SKILL.md new file mode 100644 index 0000000000..58dfb4f09a --- /dev/null +++ b/.github/skills/cuopt-lp-milp/SKILL.md @@ -0,0 +1,240 @@ +--- +name: cuopt-lp-milp +description: Solve Linear Programming (LP) and Mixed-Integer Linear Programming (MILP) with NVIDIA cuOpt. Use when the user asks about optimization with linear constraints, integer variables, scheduling, resource allocation, facility location, or production planning. +--- + +# cuOpt LP/MILP Skill + +> **Prerequisites**: Read `cuopt-user-rules/SKILL.md` first for behavior rules. + +Model and solve linear and mixed-integer linear programs using NVIDIA cuOpt's GPU-accelerated solver. + +## Before You Start: Required Questions + +**Ask these if not already clear:** + +1. **Problem formulation?** + - What are the decision variables? + - What is the objective (minimize/maximize what)? + - What are the constraints? + +2. **Variable types?** + - All continuous (LP)? + - Some integer/binary (MILP)? + +3. **Interface preference?** + - Python API (recommended for modeling) + - C API (native embedding) + - CLI (quick solve from MPS file) + - REST Server (production deployment) + +4. **Do you have an MPS file?** + - If yes, CLI or C API may be simpler + - If no, Python API is best for building the model + +## Interface Support + +| Interface | LP | MILP | +|-----------|:--:|:----:| +| Python | ✓ | ✓ | +| C API | ✓ | ✓ | +| CLI | ✓ | ✓ | +| REST | ✓ | ✓ | + +## Quick Reference: Python API + +### LP Example + +```python +from cuopt.linear_programming.problem import Problem, CONTINUOUS, MAXIMIZE +from cuopt.linear_programming.solver_settings import SolverSettings + +# Create problem +problem = Problem("MyLP") + +# Decision variables +x = problem.addVariable(lb=0, vtype=CONTINUOUS, name="x") +y = problem.addVariable(lb=0, vtype=CONTINUOUS, name="y") + +# Constraints +problem.addConstraint(2*x + 3*y <= 120, name="resource_a") +problem.addConstraint(4*x + 2*y <= 100, name="resource_b") + +# Objective +problem.setObjective(40*x + 30*y, sense=MAXIMIZE) + +# Solve +settings = SolverSettings() +settings.set_parameter("time_limit", 60) +problem.solve(settings) + +# Check status (CRITICAL: use PascalCase!) +if problem.Status.name in ["Optimal", "PrimalFeasible"]: + print(f"Objective: {problem.ObjValue}") + print(f"x = {x.getValue()}") + print(f"y = {y.getValue()}") +``` + +### MILP Example (with integer variables) + +```python +from cuopt.linear_programming.problem import Problem, CONTINUOUS, INTEGER, MINIMIZE + +problem = Problem("FacilityLocation") + +# Binary variable (integer with bounds 0-1) +open_facility = problem.addVariable(lb=0, ub=1, vtype=INTEGER, name="open") + +# Continuous variable +production = problem.addVariable(lb=0, vtype=CONTINUOUS, name="production") + +# Linking constraint: can only produce if facility is open +problem.addConstraint(production <= 1000 * open_facility, name="link") + +# Objective: fixed cost + variable cost +problem.setObjective(500*open_facility + 2*production, sense=MINIMIZE) + +# MILP-specific settings +settings = SolverSettings() +settings.set_parameter("time_limit", 120) +settings.set_parameter("mip_relative_gap", 0.01) # 1% optimality gap + +problem.solve(settings) + +# Check status +if problem.Status.name in ["Optimal", "FeasibleFound"]: + print(f"Open facility: {open_facility.getValue() > 0.5}") + print(f"Production: {production.getValue()}") +``` + +## CRITICAL: Status Checking + +**Status values use PascalCase, NOT ALL_CAPS:** + +```python +# ✅ CORRECT +if problem.Status.name in ["Optimal", "FeasibleFound"]: + print(problem.ObjValue) + +# ❌ WRONG - will silently fail! +if problem.Status.name == "OPTIMAL": # Never matches! + print(problem.ObjValue) +``` + +**LP Status Values:** `Optimal`, `NoTermination`, `NumericalError`, `PrimalInfeasible`, `DualInfeasible`, `IterationLimit`, `TimeLimit`, `PrimalFeasible` + +**MILP Status Values:** `Optimal`, `FeasibleFound`, `Infeasible`, `Unbounded`, `TimeLimit`, `NoTermination` + +## Quick Reference: C API + +```c +#include + +// CSR format for constraints +cuopt_int_t row_offsets[] = {0, 2, 4}; +cuopt_int_t col_indices[] = {0, 1, 0, 1}; +cuopt_float_t values[] = {2.0, 3.0, 4.0, 2.0}; + +// Variable types +char var_types[] = {CUOPT_CONTINUOUS, CUOPT_INTEGER}; + +cuOptCreateRangedProblem( + num_constraints, num_variables, CUOPT_MINIMIZE, + 0.0, // objective offset + objective_coefficients, + row_offsets, col_indices, values, + constraint_lower, constraint_upper, + var_lower, var_upper, + var_types, + &problem +); + +cuOptSolve(problem, settings, &solution); +cuOptGetObjectiveValue(solution, &obj_value); +``` + +## Quick Reference: CLI + +```bash +# Solve LP from MPS file +cuopt_cli problem.mps + +# With options +cuopt_cli problem.mps --time-limit 120 --mip-relative-tolerance 0.01 +``` + +## Common Modeling Patterns + +### Binary Selection +```python +# Select exactly k items from n +items = [problem.addVariable(lb=0, ub=1, vtype=INTEGER) for _ in range(n)] +problem.addConstraint(sum(items) == k) +``` + +### Big-M Linking +```python +# If y=1, then x <= 100; if y=0, x can be anything up to M +M = 10000 +problem.addConstraint(x <= 100 + M*(1 - y)) +``` + +### Piecewise Linear (SOS2) +```python +# Approximate nonlinear function with breakpoints +# Use lambda variables that sum to 1, at most 2 adjacent non-zero +``` + +## Solver Settings + +```python +settings = SolverSettings() + +# Time limit +settings.set_parameter("time_limit", 60) + +# MILP gap tolerance (stop when within X% of optimal) +settings.set_parameter("mip_relative_gap", 0.01) + +# Logging +settings.set_parameter("log_to_console", 1) +``` + +## Common Issues + +| Problem | Likely Cause | Fix | +|---------|--------------|-----| +| Status never "OPTIMAL" | Using wrong case | Use `"Optimal"` not `"OPTIMAL"` | +| Integer var has fractional value | Defined as CONTINUOUS | Use `vtype=INTEGER` | +| Infeasible | Conflicting constraints | Check constraint logic | +| Unbounded | Missing bounds | Add variable bounds | +| Slow solve | Large problem | Set time limit, increase gap tolerance | + +## Getting Dual Values (LP only) + +```python +if problem.Status.name == "Optimal": + constraint = problem.getConstraint("resource_a") + shadow_price = constraint.DualValue + print(f"Shadow price: {shadow_price}") +``` + +## Examples + +See `resources/` for complete examples: +- [Python API](resources/python_examples.md) — LP, MILP, knapsack, transportation +- [C API](resources/c_api_examples.md) — LP/MILP with build instructions +- [CLI](resources/cli_examples.md) — MPS file format and commands +- [REST Server](resources/server_examples.md) — curl and Python requests + +## When to Escalate + +Switch to **cuopt-qp** if: +- Objective has quadratic terms (x², x*y) + +Switch to **cuopt-debugging** if: +- Infeasible and can't determine why +- Numerical issues + +Switch to **cuopt-developer** if: +- User wants to modify solver internals diff --git a/.github/skills/cuopt-lp-milp/resources/c_api_examples.md b/.github/skills/cuopt-lp-milp/resources/c_api_examples.md new file mode 100644 index 0000000000..529e67df90 --- /dev/null +++ b/.github/skills/cuopt-lp-milp/resources/c_api_examples.md @@ -0,0 +1,291 @@ +# LP/MILP: C API Examples + +## Required Headers + +```c +#include // Core API +#include // Parameter name macros (CUOPT_TIME_LIMIT, etc.) +``` + +## Parameter Setting Functions + +**Important:** Use the correct function for each parameter type: + +| Function | Use For | Example | +|----------|---------|---------| +| `cuOptSetFloatParameter` | Float params (tolerances, time_limit) | `cuOptSetFloatParameter(settings, CUOPT_TIME_LIMIT, 60.0)` | +| `cuOptSetIntegerParameter` | Integer params (log_to_console, method) | `cuOptSetIntegerParameter(settings, CUOPT_LOG_TO_CONSOLE, 1)` | +| `cuOptSetParameter` | String params | `cuOptSetParameter(settings, "custom_param", "value")` | + +**Common mistake:** Using non-existent function names like `cuOptSetIntParameter` (correct: `cuOptSetIntegerParameter`). + +--- + +## Simple LP + +```c +/* + * Solve: minimize -0.2*x1 + 0.1*x2 + * subject to 3.0*x1 + 4.0*x2 <= 5.4 + * 2.7*x1 + 10.1*x2 <= 4.9 + * x1, x2 >= 0 + */ +#include +#include +#include +#include + +int main() { + cuOptOptimizationProblem problem = NULL; + cuOptSolverSettings settings = NULL; + cuOptSolution solution = NULL; + + cuopt_int_t num_variables = 2; + cuopt_int_t num_constraints = 2; + + // Constraint matrix in CSR format + cuopt_int_t row_offsets[] = {0, 2, 4}; + cuopt_int_t column_indices[] = {0, 1, 0, 1}; + cuopt_float_t values[] = {3.0, 4.0, 2.7, 10.1}; + + // Objective coefficients + cuopt_float_t objective_coefficients[] = {-0.2, 0.1}; + + // Constraint bounds (lower <= Ax <= upper) + cuopt_float_t constraint_upper_bounds[] = {5.4, 4.9}; + cuopt_float_t constraint_lower_bounds[] = {-CUOPT_INFINITY, -CUOPT_INFINITY}; + + // Variable bounds + cuopt_float_t var_lower_bounds[] = {0.0, 0.0}; + cuopt_float_t var_upper_bounds[] = {CUOPT_INFINITY, CUOPT_INFINITY}; + + // Variable types + char variable_types[] = {CUOPT_CONTINUOUS, CUOPT_CONTINUOUS}; + + cuopt_int_t status; + + // Create problem + status = cuOptCreateRangedProblem( + num_constraints, num_variables, CUOPT_MINIMIZE, + 0.0, // objective offset + objective_coefficients, + row_offsets, column_indices, values, + constraint_lower_bounds, constraint_upper_bounds, + var_lower_bounds, var_upper_bounds, + variable_types, + &problem + ); + if (status != CUOPT_SUCCESS) { + printf("Error creating problem: %d\n", status); + return 1; + } + + // Create and configure solver settings + cuOptCreateSolverSettings(&settings); + cuOptSetFloatParameter(settings, CUOPT_ABSOLUTE_PRIMAL_TOLERANCE, 0.0001); + cuOptSetFloatParameter(settings, CUOPT_TIME_LIMIT, 60.0); + + // Solve + status = cuOptSolve(problem, settings, &solution); + if (status != CUOPT_SUCCESS) { + printf("Error solving: %d\n", status); + goto cleanup; + } + + // Get results + cuopt_float_t time, objective_value; + cuopt_int_t termination_status; + + cuOptGetSolveTime(solution, &time); + cuOptGetTerminationStatus(solution, &termination_status); + cuOptGetObjectiveValue(solution, &objective_value); + + printf("Status: %d\n", termination_status); + printf("Time: %f s\n", time); + printf("Objective: %f\n", objective_value); + + // Get solution values + cuopt_float_t* sol = malloc(num_variables * sizeof(cuopt_float_t)); + cuOptGetPrimalSolution(solution, sol); + printf("x1 = %f\n", sol[0]); + printf("x2 = %f\n", sol[1]); + free(sol); + +cleanup: + cuOptDestroyProblem(&problem); + cuOptDestroySolverSettings(&settings); + cuOptDestroySolution(&solution); + return (status == CUOPT_SUCCESS) ? 0 : 1; +} +``` + +## MILP (with integer variables) + +```c +/* + * Same as LP but x1 is integer + */ +#include +#include +#include +#include + +int main() { + cuOptOptimizationProblem problem = NULL; + cuOptSolverSettings settings = NULL; + cuOptSolution solution = NULL; + + cuopt_int_t num_variables = 2; + cuopt_int_t num_constraints = 2; + + cuopt_int_t row_offsets[] = {0, 2, 4}; + cuopt_int_t column_indices[] = {0, 1, 0, 1}; + cuopt_float_t values[] = {3.0, 4.0, 2.7, 10.1}; + + cuopt_float_t objective_coefficients[] = {-0.2, 0.1}; + cuopt_float_t constraint_upper[] = {5.4, 4.9}; + cuopt_float_t constraint_lower[] = {-CUOPT_INFINITY, -CUOPT_INFINITY}; + cuopt_float_t var_lower[] = {0.0, 0.0}; + cuopt_float_t var_upper[] = {CUOPT_INFINITY, CUOPT_INFINITY}; + + // x1 = INTEGER, x2 = CONTINUOUS + char variable_types[] = {CUOPT_INTEGER, CUOPT_CONTINUOUS}; + + cuopt_int_t status = cuOptCreateRangedProblem( + num_constraints, num_variables, CUOPT_MINIMIZE, 0.0, + objective_coefficients, + row_offsets, column_indices, values, + constraint_lower, constraint_upper, + var_lower, var_upper, + variable_types, &problem + ); + if (status != CUOPT_SUCCESS) { + printf("Error creating problem: %d\n", status); + return 1; + } + + cuOptCreateSolverSettings(&settings); + cuOptSetFloatParameter(settings, CUOPT_MIP_ABSOLUTE_TOLERANCE, 0.0001); + cuOptSetFloatParameter(settings, CUOPT_MIP_RELATIVE_GAP, 0.01); + cuOptSetFloatParameter(settings, CUOPT_TIME_LIMIT, 120.0); + + status = cuOptSolve(problem, settings, &solution); + if (status != CUOPT_SUCCESS) { + printf("Error solving: %d\n", status); + goto cleanup; + } + + if (solution != NULL) { + cuopt_float_t objective_value; + cuOptGetObjectiveValue(solution, &objective_value); + printf("Objective: %f\n", objective_value); + + cuopt_float_t* sol = malloc(num_variables * sizeof(cuopt_float_t)); + if (sol == NULL) { + printf("Error: memory allocation failed\n"); + status = -1; + goto cleanup; + } + cuOptGetPrimalSolution(solution, sol); + printf("x1 (integer) = %f\n", sol[0]); + printf("x2 (continuous) = %f\n", sol[1]); + free(sol); + } + +cleanup: + cuOptDestroyProblem(&problem); + cuOptDestroySolverSettings(&settings); + cuOptDestroySolution(&solution); + return (status == CUOPT_SUCCESS) ? 0 : 1; +} +``` + +## Build & Run + +```bash +# Set paths (conda example) +export INCLUDE_PATH="${CONDA_PREFIX}/include" +export LIB_PATH="${CONDA_PREFIX}/lib" + +# Compile +gcc -I${INCLUDE_PATH} -L${LIB_PATH} -o lp_example lp_example.c -lcuopt + +# Run +LD_LIBRARY_PATH=${LIB_PATH}:$LD_LIBRARY_PATH ./lp_example +``` + +## Constants Reference + +```c +// Optimization sense +CUOPT_MINIMIZE +CUOPT_MAXIMIZE + +// Variable types +CUOPT_CONTINUOUS +CUOPT_INTEGER + +// Special values +CUOPT_INFINITY // Use for unbounded +-CUOPT_INFINITY // Use for no lower bound + +// Return codes +CUOPT_SUCCESS // 0 +``` + +## Parameter Name Constants (from constants.h) + +```c +// Float parameters (use with cuOptSetFloatParameter) +CUOPT_TIME_LIMIT // "time_limit" +CUOPT_ABSOLUTE_PRIMAL_TOLERANCE // "absolute_primal_tolerance" +CUOPT_ABSOLUTE_DUAL_TOLERANCE // "absolute_dual_tolerance" +CUOPT_RELATIVE_PRIMAL_TOLERANCE // "relative_primal_tolerance" +CUOPT_RELATIVE_DUAL_TOLERANCE // "relative_dual_tolerance" +CUOPT_MIP_ABSOLUTE_GAP // "mip_absolute_gap" +CUOPT_MIP_RELATIVE_GAP // "mip_relative_gap" +CUOPT_MIP_ABSOLUTE_TOLERANCE // "mip_absolute_tolerance" +CUOPT_MIP_RELATIVE_TOLERANCE // "mip_relative_tolerance" +CUOPT_MIP_INTEGRALITY_TOLERANCE // "mip_integrality_tolerance" + +// Integer parameters (use with cuOptSetIntegerParameter) +CUOPT_LOG_TO_CONSOLE // "log_to_console" +CUOPT_ITERATION_LIMIT // "iteration_limit" +CUOPT_METHOD // "method" (see CUOPT_METHOD_* values) +CUOPT_PDLP_SOLVER_MODE // "pdlp_solver_mode" (see CUOPT_PDLP_SOLVER_MODE_* values) +CUOPT_PRESOLVE // "presolve" +CUOPT_NUM_CPU_THREADS // "num_cpu_threads" +CUOPT_NUM_GPUS // "num_gpus" + +// Method values (for CUOPT_METHOD) +CUOPT_METHOD_CONCURRENT // 0 - Run multiple methods concurrently +CUOPT_METHOD_PDLP // 1 - PDLP solver +CUOPT_METHOD_DUAL_SIMPLEX // 2 - Dual simplex +CUOPT_METHOD_BARRIER // 3 - Barrier method + +// PDLP solver mode values (for CUOPT_PDLP_SOLVER_MODE) +CUOPT_PDLP_SOLVER_MODE_STABLE1 // 0 +CUOPT_PDLP_SOLVER_MODE_STABLE2 // 1 +CUOPT_PDLP_SOLVER_MODE_METHODICAL1 // 2 +CUOPT_PDLP_SOLVER_MODE_FAST1 // 3 +CUOPT_PDLP_SOLVER_MODE_STABLE3 // 4 +``` + +> **Complete list:** See `cpp/include/cuopt/linear_programming/constants.h` for all 50+ parameter constants including termination status codes, constraint senses, and file format constants. + +--- + +## Additional References (tested in CI) + +For more complete C examples with full error handling, see: + +| Resource | Location | +|----------|----------| +| **Constants Header** | `cpp/include/cuopt/linear_programming/constants.h` | +| C API Header | `cpp/include/cuopt/linear_programming/cuopt_c.h` | +| C API Documentation | `docs/cuopt/source/cuopt-c/lp-qp-milp/lp-qp-milp-c-api.rst` | +| Simple LP Example | `docs/cuopt/source/cuopt-c/lp-qp-milp/examples/simple_lp_example.c` | +| Simple MILP Example | `docs/cuopt/source/cuopt-c/lp-qp-milp/examples/simple_milp_example.c` | +| MPS File Example | `docs/cuopt/source/cuopt-c/lp-qp-milp/examples/mps_file_example.c` | + +The `constants.h` header contains all parameter name macros, termination status codes, method values, and constraint sense constants. diff --git a/.github/skills/cuopt-lp-milp/resources/cli_examples.md b/.github/skills/cuopt-lp-milp/resources/cli_examples.md new file mode 100644 index 0000000000..fe1d286780 --- /dev/null +++ b/.github/skills/cuopt-lp-milp/resources/cli_examples.md @@ -0,0 +1,166 @@ +# LP/MILP: CLI Examples + +## LP from MPS File + +```bash +# Create sample LP in MPS format +cat > production.mps << 'EOF' +* Production Planning: maximize 40*chairs + 30*tables +* s.t. 2*chairs + 3*tables <= 240 (wood) +* 4*chairs + 2*tables <= 200 (labor) +NAME PRODUCTION +ROWS + N PROFIT + L WOOD + L LABOR +COLUMNS + CHAIRS PROFIT -40.0 + CHAIRS WOOD 2.0 + CHAIRS LABOR 4.0 + TABLES PROFIT -30.0 + TABLES WOOD 3.0 + TABLES LABOR 2.0 +RHS + RHS1 WOOD 240.0 + RHS1 LABOR 200.0 +ENDATA +EOF + +# Solve +cuopt_cli production.mps + +# With time limit +cuopt_cli production.mps --time-limit 30 +``` + +## MILP from MPS File + +```bash +# Create MILP with integer variables +cat > facility.mps << 'EOF' +* Facility location with binary variables +NAME FACILITY +ROWS + N COST + G DEMAND1 + L CAP1 + L CAP2 +COLUMNS + MARKER 'MARKER' 'INTORG' + OPEN1 COST 100.0 + OPEN1 CAP1 50.0 + OPEN2 COST 150.0 + OPEN2 CAP2 70.0 + MARKER 'MARKER' 'INTEND' + SHIP11 COST 5.0 + SHIP11 DEMAND1 1.0 + SHIP11 CAP1 -1.0 + SHIP21 COST 7.0 + SHIP21 DEMAND1 1.0 + SHIP21 CAP2 -1.0 +RHS + RHS1 DEMAND1 30.0 +BOUNDS + BV BND1 OPEN1 + BV BND1 OPEN2 + LO BND1 SHIP11 0.0 + LO BND1 SHIP21 0.0 +ENDATA +EOF + +# Solve MILP +cuopt_cli facility.mps --time-limit 60 --mip-relative-tolerance 0.01 +``` + +## Common CLI Options + +```bash +# Show all options +cuopt_cli --help + +# Time limit (seconds) +cuopt_cli problem.mps --time-limit 120 + +# MIP gap tolerance (stop when within X% of optimal) +cuopt_cli problem.mps --mip-relative-tolerance 0.001 + +# MIP absolute tolerance +cuopt_cli problem.mps --mip-absolute-tolerance 0.0001 + +# Enable presolve +cuopt_cli problem.mps --presolve + +# Iteration limit +cuopt_cli problem.mps --iteration-limit 10000 + +# Solver method (0=auto, 1=pdlp, 2=dual_simplex, 3=barrier) +cuopt_cli problem.mps --method 1 +``` + +## MPS Format Reference + +### Required Sections (in order) + +``` +NAME problem_name +ROWS + N objective_row (N = free/objective) + L constraint1 (L = <=) + G constraint2 (G = >=) + E constraint3 (E = ==) +COLUMNS + var1 row1 coefficient + var1 row2 coefficient +RHS + rhs1 row1 value +ENDATA +``` + +### Optional: BOUNDS Section + +``` +BOUNDS + LO bnd1 var1 0.0 (lower bound) + UP bnd1 var1 100.0 (upper bound) + FX bnd1 var2 50.0 (fixed value) + FR bnd1 var3 (free variable) + BV bnd1 var4 (binary 0/1) + LI bnd1 var5 0 (integer, lower bound) + UI bnd1 var5 10 (integer, upper bound) +``` + +### Integer Markers + +``` +COLUMNS + MARKER 'MARKER' 'INTORG' + int_var1 OBJ 1.0 + int_var2 OBJ 2.0 + MARKER 'MARKER' 'INTEND' + cont_var OBJ 3.0 +``` + +## Troubleshooting + +**"Failed to parse MPS file"** +- Check for missing ENDATA +- Verify section order: NAME, ROWS, COLUMNS, RHS, [BOUNDS], ENDATA +- Check integer markers format + +**"Problem is infeasible"** +- Check constraint directions (L/G/E) +- Verify RHS values are consistent + +--- + +## Additional References (tested in CI) + +For more complete examples, read these files: + +| Example | File | Description | +|---------|------|-------------| +| Basic LP | `docs/cuopt/source/cuopt-cli/examples/lp/examples/basic_lp_example.sh` | Simple LP via CLI | +| Basic MILP | `docs/cuopt/source/cuopt-cli/examples/milp/examples/basic_milp_example.sh` | MILP with integers | +| Solver Parameters | `docs/cuopt/source/cuopt-cli/examples/lp/examples/solver_parameters_example.sh` | CLI options | + +These examples are tested by CI and represent canonical usage. diff --git a/.github/skills/cuopt-lp-milp/resources/python_examples.md b/.github/skills/cuopt-lp-milp/resources/python_examples.md new file mode 100644 index 0000000000..eaaee59d48 --- /dev/null +++ b/.github/skills/cuopt-lp-milp/resources/python_examples.md @@ -0,0 +1,257 @@ +# LP/MILP: Python API Examples + +## Linear Programming (LP) + +```python +""" +Production Planning LP: + maximize 40*chairs + 30*tables (profit) + subject to 2*chairs + 3*tables <= 240 (wood constraint) + 4*chairs + 2*tables <= 200 (labor constraint) + chairs, tables >= 0 +""" +from cuopt.linear_programming.problem import Problem, CONTINUOUS, MAXIMIZE +from cuopt.linear_programming.solver_settings import SolverSettings + +# Create problem +problem = Problem("ProductionPlanning") + +# Decision variables (continuous, non-negative) +chairs = problem.addVariable(lb=0, vtype=CONTINUOUS, name="chairs") +tables = problem.addVariable(lb=0, vtype=CONTINUOUS, name="tables") + +# Constraints +problem.addConstraint(2 * chairs + 3 * tables <= 240, name="wood") +problem.addConstraint(4 * chairs + 2 * tables <= 200, name="labor") + +# Objective: maximize profit +problem.setObjective(40 * chairs + 30 * tables, sense=MAXIMIZE) + +# Solver settings +settings = SolverSettings() +settings.set_parameter("time_limit", 60) +settings.set_parameter("log_to_console", 1) + +# Solve +problem.solve(settings) + +# Check status and extract results +status = problem.Status.name +print(f"Status: {status}") + +if status in ["Optimal", "PrimalFeasible"]: + print(f"Optimal profit: ${problem.ObjValue:.2f}") + print(f"Chairs to produce: {chairs.getValue():.1f}") + print(f"Tables to produce: {tables.getValue():.1f}") + + # Get dual values (shadow prices) + wood_constraint = problem.getConstraint("wood") + labor_constraint = problem.getConstraint("labor") + print(f"\nShadow price (wood): ${wood_constraint.DualValue:.2f} per unit") + print(f"Shadow price (labor): ${labor_constraint.DualValue:.2f} per unit") +else: + print(f"No optimal solution found. Status: {status}") +``` + +## Mixed-Integer Linear Programming (MILP) + +```python +""" +Facility Location MILP: +- Decide which warehouses to open (binary) +- Assign customers to open warehouses +- Minimize fixed costs + transportation costs +""" +from cuopt.linear_programming.problem import ( + Problem, CONTINUOUS, INTEGER, MINIMIZE +) +from cuopt.linear_programming.solver_settings import SolverSettings + +# Problem data +warehouses = ["W1", "W2", "W3"] +customers = ["C1", "C2", "C3", "C4"] + +fixed_costs = {"W1": 100, "W2": 150, "W3": 120} +capacities = {"W1": 50, "W2": 70, "W3": 60} +demands = {"C1": 20, "C2": 25, "C3": 15, "C4": 30} + +transport_cost = { + ("W1", "C1"): 5, ("W1", "C2"): 8, ("W1", "C3"): 6, ("W1", "C4"): 10, + ("W2", "C1"): 7, ("W2", "C2"): 4, ("W2", "C3"): 9, ("W2", "C4"): 5, + ("W3", "C1"): 6, ("W3", "C2"): 7, ("W3", "C3"): 4, ("W3", "C4"): 8, +} + +# Create problem +problem = Problem("FacilityLocation") + +# Binary variables: y[w] = 1 if warehouse w is open +y = {w: problem.addVariable(lb=0, ub=1, vtype=INTEGER, name=f"open_{w}") + for w in warehouses} + +# Continuous variables: x[w,c] = units shipped from w to c +x = {(w, c): problem.addVariable(lb=0, vtype=CONTINUOUS, name=f"ship_{w}_{c}") + for w in warehouses for c in customers} + +# Objective: minimize fixed + transportation costs +problem.setObjective( + sum(fixed_costs[w] * y[w] for w in warehouses) + + sum(transport_cost[w, c] * x[w, c] for w in warehouses for c in customers), + sense=MINIMIZE +) + +# Constraints: meet customer demand +for c in customers: + problem.addConstraint( + sum(x[w, c] for w in warehouses) == demands[c], + name=f"demand_{c}" + ) + +# Constraints: respect warehouse capacity (only if open) +for w in warehouses: + problem.addConstraint( + sum(x[w, c] for c in customers) <= capacities[w] * y[w], + name=f"capacity_{w}" + ) + +# Solver settings +settings = SolverSettings() +settings.set_parameter("time_limit", 120) +settings.set_parameter("mip_relative_gap", 0.01) + +# Solve +problem.solve(settings) + +# Results +status = problem.Status.name +print(f"Status: {status}") + +if status in ["Optimal", "FeasibleFound"]: + print(f"Total cost: ${problem.ObjValue:.2f}") + print("\nOpen warehouses:") + for w in warehouses: + if y[w].getValue() > 0.5: + print(f" {w} (fixed cost: ${fixed_costs[w]})") + + print("\nShipments:") + for w in warehouses: + for c in customers: + shipped = x[w, c].getValue() + if shipped > 0.01: + print(f" {w} -> {c}: {shipped:.1f} units") +``` + +## Knapsack Problem (MILP) + +```python +""" +0/1 Knapsack: select items to maximize value within weight limit +""" +from cuopt.linear_programming.problem import Problem, INTEGER, MAXIMIZE +from cuopt.linear_programming.solver_settings import SolverSettings + +items = ["laptop", "camera", "phone", "tablet", "headphones"] +values = [1000, 500, 300, 600, 150] +weights = [3, 1, 0.5, 1.5, 0.3] +max_weight = 5 + +problem = Problem("Knapsack") + +# Binary variables: x[i] = 1 if item i is selected +x = [problem.addVariable(lb=0, ub=1, vtype=INTEGER, name=items[i]) + for i in range(len(items))] + +# Objective: maximize total value +problem.setObjective(sum(values[i] * x[i] for i in range(len(items))), sense=MAXIMIZE) + +# Constraint: weight limit +problem.addConstraint(sum(weights[i] * x[i] for i in range(len(items))) <= max_weight) + +problem.solve(SolverSettings()) + +if problem.Status.name in ["Optimal", "FeasibleFound"]: + print(f"Total value: ${problem.ObjValue:.0f}") + print("Selected items:") + for i, item in enumerate(items): + if x[i].getValue() > 0.5: + print(f" {item}: value=${values[i]}, weight={weights[i]}") +``` + +## Transportation Problem (LP) + +```python +""" +Minimize shipping cost from suppliers to customers +""" +from cuopt.linear_programming.problem import Problem, CONTINUOUS, MINIMIZE + +suppliers = ["S1", "S2"] +customers = ["C1", "C2", "C3"] +supply = {"S1": 100, "S2": 150} +demand = {"C1": 80, "C2": 70, "C3": 100} +cost = { + ("S1", "C1"): 4, ("S1", "C2"): 6, ("S1", "C3"): 8, + ("S2", "C1"): 5, ("S2", "C2"): 3, ("S2", "C3"): 7, +} + +problem = Problem("Transportation") + +x = {(s, c): problem.addVariable(lb=0, vtype=CONTINUOUS, name=f"x_{s}_{c}") + for s in suppliers for c in customers} + +# Minimize total shipping cost +problem.setObjective(sum(cost[s,c] * x[s,c] for s in suppliers for c in customers), + sense=MINIMIZE) + +# Supply constraints +for s in suppliers: + problem.addConstraint(sum(x[s,c] for c in customers) <= supply[s]) + +# Demand constraints +for c in customers: + problem.addConstraint(sum(x[s,c] for s in suppliers) >= demand[c]) + +problem.solve() + +if problem.Status.name in ("Optimal", "PrimalFeasible"): + print(f"Total cost: ${problem.ObjValue:.2f}") + for s in suppliers: + for c in customers: + val = x[s,c].getValue() + if val > 0.01: + print(f" {s} -> {c}: {val:.0f} units") +``` + +## Status Checking (Critical) + +```python +# ✅ CORRECT - use PascalCase +if problem.Status.name in ["Optimal", "FeasibleFound"]: + print(problem.ObjValue) + +# ❌ WRONG - will silently fail! +if problem.Status.name == "OPTIMAL": # Never matches! + print(problem.ObjValue) + +# LP status values: Optimal, PrimalFeasible, PrimalInfeasible, +# DualInfeasible, TimeLimit, NumericalError +# MILP status values: Optimal, FeasibleFound, Infeasible, +# Unbounded, TimeLimit, NoTermination +``` + +--- + +## Additional References (tested in CI) + +For more complete examples, read these files: + +| Example | File | Description | +|---------|------|-------------| +| Simple LP | `docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_lp_example.py` | Basic LP setup | +| Simple MILP | `docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_milp_example.py` | Integer variables | +| Production Planning | `docs/cuopt/source/cuopt-python/lp-qp-milp/examples/production_planning_example.py` | Real-world LP | +| Expressions | `docs/cuopt/source/cuopt-python/lp-qp-milp/examples/expressions_constraints_example.py` | Advanced constraint syntax | +| Incumbent Solutions | `docs/cuopt/source/cuopt-python/lp-qp-milp/examples/incumbent_solutions_example.py` | Tracking MIP progress | +| Warmstart | `docs/cuopt/source/cuopt-python/lp-qp-milp/examples/pdlp_warmstart_example.py` | Warm starting LP | +| Solution Handling | `docs/cuopt/source/cuopt-python/lp-qp-milp/examples/solution_example.py` | Working with results | + +These examples are tested by CI (`ci/test_doc_examples.sh`) and represent canonical usage. diff --git a/.github/skills/cuopt-lp-milp/resources/server_examples.md b/.github/skills/cuopt-lp-milp/resources/server_examples.md new file mode 100644 index 0000000000..521d8a6ead --- /dev/null +++ b/.github/skills/cuopt-lp-milp/resources/server_examples.md @@ -0,0 +1,208 @@ +# LP/MILP: REST Server Examples + +## LP Request (curl) + +```bash +# Production Planning LP via REST +# maximize 40*chairs + 30*tables +# s.t. 2*chairs + 3*tables <= 240 +# 4*chairs + 2*tables <= 200 + +REQID=$(curl -s -X POST "http://localhost:8000/cuopt/request" \ + -H "Content-Type: application/json" \ + -H "CLIENT-VERSION: custom" \ + -d '{ + "csr_constraint_matrix": { + "offsets": [0, 2, 4], + "indices": [0, 1, 0, 1], + "values": [2.0, 3.0, 4.0, 2.0] + }, + "constraint_bounds": { + "upper_bounds": [240.0, 200.0], + "lower_bounds": ["ninf", "ninf"] + }, + "objective_data": { + "coefficients": [40.0, 30.0], + "scalability_factor": 1.0, + "offset": 0.0 + }, + "variable_bounds": { + "upper_bounds": ["inf", "inf"], + "lower_bounds": [0.0, 0.0] + }, + "maximize": true, + "solver_config": { + "tolerances": {"optimality": 0.0001}, + "time_limit": 60 + } + }' | jq -r '.reqId') + +echo "Request ID: $REQID" + +# Get solution +sleep 2 +curl -s "http://localhost:8000/cuopt/solution/$REQID" \ + -H "CLIENT-VERSION: custom" | jq . +``` + +## MILP Request (curl) + +```bash +# Add integer variable types +REQID=$(curl -s -X POST "http://localhost:8000/cuopt/request" \ + -H "Content-Type: application/json" \ + -H "CLIENT-VERSION: custom" \ + -d '{ + "csr_constraint_matrix": { + "offsets": [0, 2, 4], + "indices": [0, 1, 0, 1], + "values": [2.0, 3.0, 4.0, 2.0] + }, + "constraint_bounds": { + "upper_bounds": [240.0, 200.0], + "lower_bounds": ["ninf", "ninf"] + }, + "objective_data": { + "coefficients": [40.0, 30.0] + }, + "variable_bounds": { + "upper_bounds": ["inf", "inf"], + "lower_bounds": [0.0, 0.0] + }, + "variable_types": ["integer", "continuous"], + "maximize": true, + "solver_config": { + "time_limit": 120, + "tolerances": { + "mip_relative_gap": 0.01 + } + } + }' | jq -r '.reqId') + +echo "Request ID: $REQID" + +# Poll for solution (MILP may take longer than LP) +while true; do + RESULT=$(curl -s "http://localhost:8000/cuopt/solution/$REQID" \ + -H "CLIENT-VERSION: custom") + STATUS=$(echo "$RESULT" | jq -r '.response.status // empty') + if [ -n "$STATUS" ]; then + echo "$RESULT" | jq . + break + fi + sleep 2 +done +``` + +## LP Request (Python) + +```python +import requests +import time + +SERVER = "http://localhost:8000" +HEADERS = {"Content-Type": "application/json", "CLIENT-VERSION": "custom"} + +payload = { + "csr_constraint_matrix": { + "offsets": [0, 2, 4], + "indices": [0, 1, 0, 1], + "values": [2.0, 3.0, 4.0, 2.0] + }, + "constraint_bounds": { + "upper_bounds": [240.0, 200.0], + "lower_bounds": ["ninf", "ninf"] + }, + "objective_data": { + "coefficients": [40.0, 30.0], + "scalability_factor": 1.0, + "offset": 0.0 + }, + "variable_bounds": { + "upper_bounds": ["inf", "inf"], + "lower_bounds": [0.0, 0.0] + }, + "maximize": True, + "solver_config": { + "time_limit": 60 + } +} + +# Submit +response = requests.post(f"{SERVER}/cuopt/request", json=payload, headers=HEADERS) +req_id = response.json()["reqId"] +print(f"Submitted: {req_id}") + +# Poll for solution +for _ in range(30): + response = requests.get(f"{SERVER}/cuopt/solution/{req_id}", headers=HEADERS) + result = response.json() + + if "response" in result: + print(f"Status: {result['response'].get('status')}") + print(f"Objective: {result['response'].get('objective_value')}") + print(f"Solution: {result['response'].get('primal_solution')}") + break + time.sleep(1) +``` + +## CSR Matrix Format + +The constraint matrix uses Compressed Sparse Row (CSR) format: + +``` +Matrix: [2, 3] (row 0: 2*x0 + 3*x1) + [4, 2] (row 1: 4*x0 + 2*x1) + +CSR format: + offsets: [0, 2, 4] # Row pointers + indices: [0, 1, 0, 1] # Column indices + values: [2.0, 3.0, 4.0, 2.0] # Non-zero values +``` + +## Special Values + +```json +{ + "constraint_bounds": { + "lower_bounds": ["ninf", "ninf"], + "upper_bounds": [100.0, "inf"] + } +} +``` + +- `"ninf"` — negative infinity (-∞) +- `"inf"` — positive infinity (+∞) + +## Variable Types + +```json +{ + "variable_types": ["continuous", "integer", "binary"] +} +``` + +- `"continuous"` - real-valued +- `"integer"` - integer-valued +- `"binary"` - 0 or 1 only + +--- + +## Additional References (tested in CI) + +For more complete examples, read these files: + +| Example | File | Description | +|---------|------|-------------| +| Basic LP (Python) | `docs/cuopt/source/cuopt-server/examples/lp/examples/basic_lp_example.py` | LP via REST | +| Basic LP (curl) | `docs/cuopt/source/cuopt-server/examples/lp/examples/basic_lp_example.sh` | LP shell script | +| MPS Input | `docs/cuopt/source/cuopt-server/examples/lp/examples/mps_file_example.py` | MPS file format | +| MPS DataModel | `docs/cuopt/source/cuopt-server/examples/lp/examples/mps_datamodel_example.py` | MPS in payload | +| Warmstart | `docs/cuopt/source/cuopt-server/examples/lp/examples/warmstart_example.py` | Warm starting | +| Basic MILP (Python) | `docs/cuopt/source/cuopt-server/examples/milp/examples/basic_milp_example.py` | MILP via REST | +| Basic MILP (curl) | `docs/cuopt/source/cuopt-server/examples/milp/examples/basic_milp_example.sh` | MILP shell script | +| Incumbent Callback | `docs/cuopt/source/cuopt-server/examples/milp/examples/incumbent_callback_example.py` | MIP progress tracking | +| Abort Job | `docs/cuopt/source/cuopt-server/examples/milp/examples/abort_job_example.py` | Canceling requests | +| Batch Mode | `docs/cuopt/source/cuopt-server/examples/lp/examples/batch_mode_example.sh` | Multiple problems | + +These examples are tested by CI (`ci/test_doc_examples.sh`) and represent canonical usage. diff --git a/.github/skills/cuopt-qp/SKILL.md b/.github/skills/cuopt-qp/SKILL.md new file mode 100644 index 0000000000..5fd7d83af6 --- /dev/null +++ b/.github/skills/cuopt-qp/SKILL.md @@ -0,0 +1,195 @@ +--- +name: cuopt-qp +description: Solve Quadratic Programming (QP) with NVIDIA cuOpt. Use when the user asks about quadratic objectives, portfolio optimization, variance minimization, or least squares problems. Note that QP support is currently in beta. +--- + +# cuOpt QP Skill + +> **Prerequisites**: Read `cuopt-user-rules/SKILL.md` first for behavior rules. + +Model and solve quadratic programs using NVIDIA cuOpt. **QP support is currently in beta.** + +## Before You Start: Required Questions + +**Ask these if not already clear:** + +1. **Is this actually QP?** + - Does the objective have x² or x*y terms? + - Are constraints still linear? + - (If constraints are also quadratic, cuOpt doesn't support that) + +2. **Minimize or maximize?** + - ⚠️ **QP only supports MINIMIZE** + - For maximization, negate the objective + +3. **Is the quadratic form convex?** + - Q matrix should be positive semi-definite for minimization + - Non-convex QP may not solve correctly + +## Critical Constraints + +### ⚠️ MINIMIZE ONLY + +**QP objectives MUST be minimization.** The solver rejects maximize for QP. + +```python +# ❌ WRONG - will fail +problem.setObjective(x*x + y*y, sense=MAXIMIZE) + +# ✅ CORRECT - minimize instead +# To maximize f(x), minimize -f(x) +problem.setObjective(-(x*x + y*y), sense=MINIMIZE) +``` + +### Interface Support + +| Interface | QP Support | +|-----------|:----------:| +| Python | ✓ (beta) | +| C API | ✓ | +| REST | ✗ | +| CLI | ✗ | + +## Quick Reference: Python API + +### Portfolio Optimization Example + +```python +""" +Minimize portfolio variance (risk): + minimize x^T * Q * x + subject to sum(x) = 1 (fully invested) + r^T * x >= target (minimum return) + x >= 0 (no short selling) +""" +from cuopt.linear_programming.problem import Problem, CONTINUOUS, MINIMIZE +from cuopt.linear_programming.solver_settings import SolverSettings + +problem = Problem("Portfolio") + +# Portfolio weights (decision variables) +x1 = problem.addVariable(lb=0, ub=1, vtype=CONTINUOUS, name="stock_a") +x2 = problem.addVariable(lb=0, ub=1, vtype=CONTINUOUS, name="stock_b") +x3 = problem.addVariable(lb=0, ub=1, vtype=CONTINUOUS, name="stock_c") + +# Expected returns +r1, r2, r3 = 0.12, 0.08, 0.05 + +# Quadratic objective: variance = x^T * Q * x +# Q = [[0.04, 0.01, 0.005], +# [0.01, 0.02, 0.008], +# [0.005, 0.008, 0.01]] +# Expanded: 0.04*x1² + 0.02*x2² + 0.01*x3² + 2*0.01*x1*x2 + ... +problem.setObjective( + 0.04*x1*x1 + 0.02*x2*x2 + 0.01*x3*x3 + + 0.02*x1*x2 + 0.01*x1*x3 + 0.016*x2*x3, + sense=MINIMIZE # MUST be minimize for QP! +) + +# Linear constraints +problem.addConstraint(x1 + x2 + x3 == 1, name="budget") +problem.addConstraint(r1*x1 + r2*x2 + r3*x3 >= 0.08, name="min_return") + +# Solve +settings = SolverSettings() +settings.set_parameter("time_limit", 60) +problem.solve(settings) + +# Check results +if problem.Status.name in ["Optimal", "PrimalFeasible"]: + print(f"Variance: {problem.ObjValue:.6f}") + print(f"Std Dev: {problem.ObjValue**0.5:.4f}") + print(f"Allocation: A={x1.getValue():.2%}, B={x2.getValue():.2%}, C={x3.getValue():.2%}") +``` + +### Least Squares Example + +```python +""" +Minimize ||Ax - b||² = x^T*A^T*A*x - 2*b^T*A*x + b^T*b +""" +problem = Problem("LeastSquares") + +x = problem.addVariable(lb=-100, ub=100, vtype=CONTINUOUS, name="x") +y = problem.addVariable(lb=-100, ub=100, vtype=CONTINUOUS, name="y") + +# Quadratic objective: (x-3)² + (y-4)² = x² + y² - 6x - 8y + 25 +problem.setObjective( + x*x + y*y - 6*x - 8*y + 25, + sense=MINIMIZE +) + +problem.solve(SolverSettings()) + +print(f"x = {x.getValue()}") # Should be ~3 +print(f"y = {y.getValue()}") # Should be ~4 +``` + +## Formulating Quadratic Objectives + +### From Covariance Matrix + +Given covariance matrix Q and weights x: +``` +variance = x^T * Q * x = Σᵢ Σⱼ Qᵢⱼ * xᵢ * xⱼ +``` + +Expand manually: +```python +# Q = [[a, b], [b, c]] +# x^T * Q * x = a*x1² + 2b*x1*x2 + c*x2² +objective = a*x1*x1 + 2*b*x1*x2 + c*x2*x2 +``` + +### Maximization Workaround + +To maximize `f(x) = -x² + 4x`: +```python +# maximize -x² + 4x +# = minimize -(-x² + 4x) +# = minimize x² - 4x +problem.setObjective(x*x - 4*x, sense=MINIMIZE) +# Then negate the objective value for the true maximum +true_max = -problem.ObjValue +``` + +## Status Checking + +Same as LP/MILP - use PascalCase: + +```python +if problem.Status.name in ["Optimal", "PrimalFeasible"]: + print(f"Optimal variance: {problem.ObjValue}") +``` + +## Common Issues + +| Problem | Likely Cause | Fix | +|---------|--------------|-----| +| "Quadratic problems must be minimized" | Using MAXIMIZE | Use MINIMIZE, negate objective | +| Poor convergence | Non-convex Q | Ensure Q is positive semi-definite | +| NumericalError | Ill-conditioned Q | Scale variables, regularize | +| Slow solve | Large dense Q | Check if problem can be simplified | + +## Solver Notes + +- QP uses **Barrier method** internally (different from LP/MILP defaults) +- May be more sensitive to numerical issues than LP +- Beta status means API may change in future versions + +## Examples + +See `resources/` for complete examples: +- [Python API](resources/python_examples.md) — portfolio, least squares, maximization workaround + +## When to Escalate + +Switch to **cuopt-lp-milp** if: +- Objective is actually linear (no x² or x*y terms) + +Switch to **cuopt-debugging** if: +- Numerical errors +- Unexpected results + +Switch to **cuopt-developer** if: +- Need features not in beta QP diff --git a/.github/skills/cuopt-qp/resources/python_examples.md b/.github/skills/cuopt-qp/resources/python_examples.md new file mode 100644 index 0000000000..80b9802dbb --- /dev/null +++ b/.github/skills/cuopt-qp/resources/python_examples.md @@ -0,0 +1,198 @@ +# QP: Python API Examples + +## Portfolio Optimization + +```python +""" +Minimize portfolio variance (risk): + minimize x^T * Q * x + subject to sum(x) = 1 (fully invested) + r^T * x >= target (minimum return) + x >= 0 (no short selling) + +Note: QP is beta and MUST use MINIMIZE (not MAXIMIZE) +""" +from cuopt.linear_programming.problem import Problem, CONTINUOUS, MINIMIZE +from cuopt.linear_programming.solver_settings import SolverSettings + +problem = Problem("Portfolio") + +# Portfolio weights (decision variables) +x1 = problem.addVariable(lb=0, ub=1, vtype=CONTINUOUS, name="stock_a") +x2 = problem.addVariable(lb=0, ub=1, vtype=CONTINUOUS, name="stock_b") +x3 = problem.addVariable(lb=0, ub=1, vtype=CONTINUOUS, name="stock_c") + +# Expected returns +r1, r2, r3 = 0.12, 0.08, 0.05 # 12%, 8%, 5% +target_return = 0.08 + +# Covariance matrix Q: +# [[0.04, 0.01, 0.005], +# [0.01, 0.02, 0.008], +# [0.005, 0.008, 0.01]] +# +# Quadratic objective: x^T * Q * x +# Expanded: 0.04*x1² + 0.02*x2² + 0.01*x3² + 2*0.01*x1*x2 + 2*0.005*x1*x3 + 2*0.008*x2*x3 + +problem.setObjective( + 0.04*x1*x1 + 0.02*x2*x2 + 0.01*x3*x3 + + 0.02*x1*x2 + 0.01*x1*x3 + 0.016*x2*x3, + sense=MINIMIZE # MUST be MINIMIZE for QP! +) + +# Linear constraints +problem.addConstraint(x1 + x2 + x3 == 1, name="budget") +problem.addConstraint(r1*x1 + r2*x2 + r3*x3 >= target_return, name="min_return") + +# Solve +settings = SolverSettings() +settings.set_parameter("time_limit", 60) +problem.solve(settings) + +# Results +if problem.Status.name in ["Optimal", "PrimalFeasible"]: + print(f"Portfolio variance: {problem.ObjValue:.6f}") + print(f"Portfolio std dev: {problem.ObjValue**0.5:.4f}") + print(f"\nAllocation:") + print(f" Stock A: {x1.getValue()*100:.2f}%") + print(f" Stock B: {x2.getValue()*100:.2f}%") + print(f" Stock C: {x3.getValue()*100:.2f}%") + + actual_return = r1*x1.getValue() + r2*x2.getValue() + r3*x3.getValue() + print(f"\nExpected return: {actual_return*100:.2f}%") +``` + +## Least Squares + +```python +""" +Minimize ||Ax - b||² = (Ax-b)^T(Ax-b) + +Example: Find point closest to (3, 4) +minimize (x-3)² + (y-4)² = x² - 6x + 9 + y² - 8y + 16 +""" +from cuopt.linear_programming.problem import Problem, CONTINUOUS, MINIMIZE +from cuopt.linear_programming.solver_settings import SolverSettings + +problem = Problem("LeastSquares") + +x = problem.addVariable(lb=-100, ub=100, vtype=CONTINUOUS, name="x") +y = problem.addVariable(lb=-100, ub=100, vtype=CONTINUOUS, name="y") + +# Quadratic objective: (x-3)² + (y-4)² +# Expanded: x² + y² - 6x - 8y + 25 +problem.setObjective( + x*x + y*y - 6*x - 8*y + 25, + sense=MINIMIZE +) + +result = problem.solve(SolverSettings()) + +if problem.Status.name in ["Optimal", "PrimalFeasible"]: + print(f"x = {x.getValue():.4f}") # Should be ~3 + print(f"y = {y.getValue():.4f}") # Should be ~4 +else: + raise RuntimeError(f"Solver failed with status: {problem.Status.name}") +``` + +## Quadratic with Linear Constraints + +```python +""" +minimize x² + y² + z² +subject to x + y + z = 10 + x >= 0, y >= 0, z >= 0 +""" +from cuopt.linear_programming.problem import Problem, CONTINUOUS, MINIMIZE + +problem = Problem("QuadraticConstrained") + +x = problem.addVariable(lb=0, vtype=CONTINUOUS, name="x") +y = problem.addVariable(lb=0, vtype=CONTINUOUS, name="y") +z = problem.addVariable(lb=0, vtype=CONTINUOUS, name="z") + +problem.setObjective(x*x + y*y + z*z, sense=MINIMIZE) +problem.addConstraint(x + y + z == 10) + +problem.solve() + +if problem.Status.name == "Optimal": + print(f"x = {x.getValue():.4f}") + print(f"y = {y.getValue():.4f}") + print(f"z = {z.getValue():.4f}") + print(f"Objective = {problem.ObjValue:.4f}") +``` + +## Maximization Workaround + +```python +""" +QP only supports MINIMIZE. +To maximize f(x), minimize -f(x). + +Example: maximize -x² + 4x (parabola with max at x=2) +""" +from cuopt.linear_programming.problem import Problem, CONTINUOUS, MINIMIZE + +problem = Problem("MaxWorkaround") + +x = problem.addVariable(lb=0, ub=10, vtype=CONTINUOUS, name="x") + +# Want to maximize: -x² + 4x +# Instead minimize: -(-x² + 4x) = x² - 4x +problem.setObjective(x*x - 4*x, sense=MINIMIZE) + +problem.solve() + +if problem.Status.name in ["Optimal", "PrimalFeasible"]: + print(f"x = {x.getValue():.4f}") # Should be 2 + print(f"Minimized value = {problem.ObjValue:.4f}") # Should be -4 + print(f"Original maximum = {-problem.ObjValue:.4f}") # Should be 4 +else: + print(f"Solver did not find optimal solution. Status: {problem.Status.name}") +``` + +## Expanding Covariance Matrix + +Given covariance matrix Q and weight vector x: + +```python +# Covariance matrix +Q = [ + [0.04, 0.01, 0.005], + [0.01, 0.02, 0.008], + [0.005, 0.008, 0.01] +] + +# Expansion: x^T * Q * x +# = Q[0,0]*x1² + Q[1,1]*x2² + Q[2,2]*x3² +# + 2*Q[0,1]*x1*x2 + 2*Q[0,2]*x1*x3 + 2*Q[1,2]*x2*x3 +# +# = 0.04*x1*x1 + 0.02*x2*x2 + 0.01*x3*x3 +# + 0.02*x1*x2 + 0.01*x1*x3 + 0.016*x2*x3 + +objective = ( + Q[0][0]*x1*x1 + Q[1][1]*x2*x2 + Q[2][2]*x3*x3 + + 2*Q[0][1]*x1*x2 + 2*Q[0][2]*x1*x3 + 2*Q[1][2]*x2*x3 +) +``` + +## Critical Reminders + +1. **MINIMIZE only** - solver rejects MAXIMIZE for QP +2. **Convexity** - Q should be positive semi-definite +3. **Beta status** - API may change in future versions +4. **Status checking** - use PascalCase: `"Optimal"` not `"OPTIMAL"` + +--- + +## Additional References (tested in CI) + +For more complete examples, read these files: + +| Example | File | Description | +|---------|------|-------------| +| Simple QP | `docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_qp_example.py` | Basic QP setup | +| QP with Matrix | `docs/cuopt/source/cuopt-python/lp-qp-milp/examples/qp_matrix_example.py` | CSR matrix format for Q | + +These examples are tested by CI (`ci/test_doc_examples.sh`) and represent canonical usage. diff --git a/.github/skills/cuopt-routing/SKILL.md b/.github/skills/cuopt-routing/SKILL.md new file mode 100644 index 0000000000..e634da7dba --- /dev/null +++ b/.github/skills/cuopt-routing/SKILL.md @@ -0,0 +1,298 @@ +--- +name: cuopt-routing +description: Solve vehicle routing problems (VRP, TSP, PDP) with NVIDIA cuOpt. Use when the user asks about delivery optimization, fleet routing, time windows, capacities, pickup-delivery pairs, or traveling salesman problems. +--- + +# cuOpt Routing Skill + +> **Prerequisites**: Read `cuopt-user-rules/SKILL.md` first for behavior rules. + +Model and solve vehicle routing problems using NVIDIA cuOpt's GPU-accelerated solver. + +## Before You Start: Required Questions + +**Ask these if not already clear:** + +1. **Problem type?** + - TSP (single vehicle, visit all locations) + - VRP (multiple vehicles, capacity constraints) + - PDP (pickup and delivery pairs) + +2. **What constraints?** + - Time windows (earliest/latest arrival)? + - Vehicle capacities? + - Service times at locations? + - Multiple depots? + - Vehicle-specific start/end locations? + +3. **What data do you have?** + - Cost/distance matrix or coordinates? + - Demand per location? + - Fleet size fixed or to optimize? + +4. **Interface preference?** + - Python API (in-process) + - REST Server (production/async) + +## Interface Support + +| Interface | Routing Support | +|-----------|:---------------:| +| Python | ✓ | +| REST | ✓ | +| C API | ✗ | +| CLI | ✗ | + +## Quick Reference: Python API + +### Minimal VRP Example + +```python +import cudf +from cuopt import routing + +# Cost matrix (n_locations x n_locations) +cost_matrix = cudf.DataFrame([ + [0, 10, 15, 20], + [10, 0, 12, 18], + [15, 12, 0, 10], + [20, 18, 10, 0], +], dtype="float32") + +# Build data model +dm = routing.DataModel( + n_locations=4, # Total locations including depot + n_fleet=2, # Number of vehicles + n_orders=3 # Orders to fulfill (locations 1,2,3) +) + +# Required: cost matrix +dm.add_cost_matrix(cost_matrix) + +# Required: order locations (which location each order is at) +dm.set_order_locations(cudf.Series([1, 2, 3])) + +# Solve +solution = routing.Solve(dm, routing.SolverSettings()) + +# Check result +if solution.get_status() == 0: # SUCCESS + solution.display_routes() +``` + +### Adding Constraints + +```python +# Time windows (need transit time matrix) +dm.add_transit_time_matrix(transit_time_matrix) +dm.set_order_time_windows( + cudf.Series([0, 10, 20]), # earliest + cudf.Series([50, 60, 70]) # latest +) + +# Capacities +dm.add_capacity_dimension( + "weight", + cudf.Series([20, 30, 25]), # demand per order + cudf.Series([100, 100]) # capacity per vehicle +) + +# Service times +dm.set_order_service_times(cudf.Series([5, 5, 5])) + +# Vehicle locations (start/end) +dm.set_vehicle_locations( + cudf.Series([0, 0]), # start at depot + cudf.Series([0, 0]) # return to depot +) + +# Vehicle time windows +dm.set_vehicle_time_windows( + cudf.Series([0, 0]), # earliest start + cudf.Series([200, 200]) # latest return +) +``` + +### Pickup and Delivery (PDP) + +```python +# Demand: positive=pickup, negative=delivery (must sum to 0 per pair) +demand = cudf.Series([10, -10, 15, -15]) + +# Pair indices: order 0 pairs with 1, order 2 pairs with 3 +dm.set_pickup_delivery_pairs( + cudf.Series([0, 2]), # pickup order indices + cudf.Series([1, 3]) # delivery order indices +) +``` + +### Precedence Constraints + +Use `add_order_precedence()` to require certain orders to be visited before others. + +**Important:** This is a per-node API — call it once for each order that has predecessors. + +```python +import numpy as np + +# Order 2 must come after orders 0 and 1 +dm.add_order_precedence( + node_id=2, # this order + preceding_nodes=np.array([0, 1]) # must come after these +) + +# Order 3 must come after order 2 +dm.add_order_precedence( + node_id=3, + preceding_nodes=np.array([2]) +) +``` + +**Rules:** +- Call once per order that has predecessors +- `preceding_nodes` is a numpy array of order indices +- Circular dependencies are NOT allowed (A before B before A) +- Orders without precedence constraints don't need a call + +**Example: Assembly sequence** +```python +# Task B requires Task A to be done first +# Task C requires Tasks A and B to be done first +dm.add_order_precedence(1, np.array([0])) # B after A +dm.add_order_precedence(2, np.array([0, 1])) # C after A and B +``` + +## Quick Reference: REST Server + +### Terminology Difference + +| Concept | Python API | REST Server | +|---------|------------|-------------| +| Jobs | `order_locations` | `task_locations` | +| Time windows | `set_order_time_windows()` | `task_time_windows` | +| Service times | `set_order_service_times()` | `service_times` | + +### Minimal REST Payload + +```json +{ + "cost_matrix_data": { + "data": {"0": [[0,10,15],[10,0,12],[15,12,0]]} + }, + "travel_time_matrix_data": { + "data": {"0": [[0,10,15],[10,0,12],[15,12,0]]} + }, + "task_data": { + "task_locations": [1, 2] + }, + "fleet_data": { + "vehicle_locations": [[0, 0]], + "capacities": [[100]] + }, + "solver_config": { + "time_limit": 10 + } +} +``` + +## Solution Checking + +```python +status = solution.get_status() +# 0 = SUCCESS +# 1 = FAIL +# 2 = TIMEOUT +# 3 = EMPTY + +if status == 0: + solution.display_routes() + route_df = solution.get_route() + total_cost = solution.get_total_objective() +else: + print(f"Error: {solution.get_error_message()}") + infeasible = solution.get_infeasible_orders() + if len(infeasible) > 0: + print(f"Infeasible orders: {infeasible.to_list()}") +``` + +## Solution DataFrame Schema + +`solution.get_route()` returns a `cudf.DataFrame` with these columns: + +| Column | Type | Description | +|--------|------|-------------| +| `route` | int | Order/task index in the route sequence | +| `truck_id` | int | Vehicle ID assigned to this stop | +| `location` | int | Location index (0 = depot typically) | +| `arrival_stamp` | float | Arrival time at this location | + +**Example output:** +``` + route arrival_stamp truck_id location +0 0 0.0 1 0 # Vehicle 1 starts at depot +1 3 2.0 1 3 # Vehicle 1 visits location 3 +2 2 4.0 1 2 # Vehicle 1 visits location 2 +3 0 5.0 1 0 # Vehicle 1 returns to depot +4 0 0.0 0 0 # Vehicle 0 starts at depot +5 1 1.0 0 1 # Vehicle 0 visits location 1 +6 0 3.0 0 0 # Vehicle 0 returns to depot +``` + +**Working with results:** +```python +route_df = solution.get_route() + +# Routes per vehicle +for vid in route_df["truck_id"].unique().to_arrow().tolist(): + vehicle_route = route_df[route_df["truck_id"] == vid] + locations = vehicle_route["location"].to_arrow().tolist() + print(f"Vehicle {vid}: {locations}") + +# Total travel time +max_arrival = route_df["arrival_stamp"].max() +``` + +## Common Issues + +| Problem | Likely Cause | Fix | +|---------|--------------|-----| +| Empty solution | Time windows too tight | Widen windows or check travel times | +| Infeasible orders | Demand > capacity | Increase fleet or capacity | +| Status != 0 | Missing transit time matrix | Add `add_transit_time_matrix()` when using time windows | +| Wrong route cost | Matrix not symmetric | Check cost_matrix values | + +## Data Type Requirements + +```python +# Always use explicit dtypes +cost_matrix = cost_matrix.astype("float32") +order_locations = cudf.Series([...], dtype="int32") +demand = cudf.Series([...], dtype="int32") +vehicle_capacity = cudf.Series([...], dtype="int32") +time_windows = cudf.Series([...], dtype="int32") +``` + +## Solver Settings + +```python +ss = routing.SolverSettings() +ss.set_time_limit(30) # seconds +ss.set_verbose_mode(True) # enable progress output +ss.set_error_logging_mode(True) # log constraint errors if infeasible +``` + +## Examples + +See `resources/` for complete examples: +- [Python API](resources/python_examples.md) — VRP, PDP, multi-depot +- [REST Server](resources/server_examples.md) — curl and Python requests + +## When to Escalate + +Switch to **cuopt-debugging** if: +- Solution is infeasible and you can't determine why +- Performance is unexpectedly slow + +Switch to **cuopt-developer** if: +- User wants to modify solver behavior +- User wants to add new constraint types diff --git a/.github/skills/cuopt-routing/resources/python_examples.md b/.github/skills/cuopt-routing/resources/python_examples.md new file mode 100644 index 0000000000..4ff694d35a --- /dev/null +++ b/.github/skills/cuopt-routing/resources/python_examples.md @@ -0,0 +1,249 @@ +# Routing: Python API Examples + +## VRP with Time Windows & Capacities + +```python +""" +Vehicle Routing Problem with: +- 1 depot (location 0) +- 5 customer locations (1-5) +- 2 vehicles with capacity 100 each +- Time windows for each location +- Demand at each customer +""" +import cudf +from cuopt import routing + +# Cost/distance matrix (6x6: depot + 5 customers) +cost_matrix = cudf.DataFrame([ + [0, 10, 15, 20, 25, 30], # From depot + [10, 0, 12, 18, 22, 28], # From customer 1 + [15, 12, 0, 10, 15, 20], # From customer 2 + [20, 18, 10, 0, 8, 15], # From customer 3 + [25, 22, 15, 8, 0, 10], # From customer 4 + [30, 28, 20, 15, 10, 0], # From customer 5 +], dtype="float32") + +# Also use as transit time matrix (same values for simplicity) +transit_time_matrix = cost_matrix.copy(deep=True) + +# Order data (customers 1-5) +order_locations = cudf.Series([1, 2, 3, 4, 5]) # Location indices for orders + +# Demand at each customer (single capacity dimension) +demand = cudf.Series([20, 30, 25, 15, 35], dtype="int32") + +# Vehicle capacities (must match demand dimensions) +vehicle_capacity = cudf.Series([100, 100], dtype="int32") + +# Time windows for orders [earliest, latest] +order_earliest = cudf.Series([0, 10, 20, 0, 30], dtype="int32") +order_latest = cudf.Series([50, 60, 70, 80, 90], dtype="int32") + +# Service time at each customer +service_times = cudf.Series([5, 5, 5, 5, 5], dtype="int32") + +# Fleet configuration +n_fleet = 2 + +# Vehicle start/end locations (both start and return to depot) +vehicle_start = cudf.Series([0, 0], dtype="int32") +vehicle_end = cudf.Series([0, 0], dtype="int32") + +# Vehicle time windows (operating hours) +vehicle_earliest = cudf.Series([0, 0], dtype="int32") +vehicle_latest = cudf.Series([200, 200], dtype="int32") + +# Build the data model +dm = routing.DataModel( + n_locations=cost_matrix.shape[0], + n_fleet=n_fleet, + n_orders=len(order_locations) +) + +# Add matrices +dm.add_cost_matrix(cost_matrix) +dm.add_transit_time_matrix(transit_time_matrix) + +# Add order data +dm.set_order_locations(order_locations) +dm.set_order_time_windows(order_earliest, order_latest) +dm.set_order_service_times(service_times) + +# Add capacity dimension (name, demand_per_order, capacity_per_vehicle) +dm.add_capacity_dimension("weight", demand, vehicle_capacity) + +# Add fleet data +dm.set_vehicle_locations(vehicle_start, vehicle_end) +dm.set_vehicle_time_windows(vehicle_earliest, vehicle_latest) + +# Configure solver +ss = routing.SolverSettings() +ss.set_time_limit(10) # seconds + +# Solve +solution = routing.Solve(dm, ss) + +# Check solution status +print(f"Status: {solution.get_status()}") + +# Display routes +if solution.get_status() == 0: # Success + print("\n--- Solution Found ---") + solution.display_routes() + + # Get detailed route data + route_df = solution.get_route() + print("\nDetailed route data:") + print(route_df) + + # Get objective value (total cost) + print(f"\nTotal cost: {solution.get_total_objective()}") +else: + print("No feasible solution found (status != 0).") +``` + +## Pickup and Delivery Problem (PDP) + +```python +""" +Pickup and Delivery Problem: +- Items must be picked up from one location and delivered to another +- Same vehicle must do both pickup and delivery +- Pickup must occur before delivery +""" +import cudf +from cuopt import routing + +# Cost matrix (depot + 4 locations) +cost_matrix = cudf.DataFrame([ + [0, 10, 20, 30, 40], + [10, 0, 15, 25, 35], + [20, 15, 0, 10, 20], + [30, 25, 10, 0, 15], + [40, 35, 20, 15, 0], +], dtype="float32") + +transit_time_matrix = cost_matrix.copy(deep=True) + +n_fleet = 2 +n_orders = 4 # 2 pickup-delivery pairs = 4 orders + +# Orders: pickup at loc 1 -> deliver at loc 2, pickup at loc 3 -> deliver at loc 4 +order_locations = cudf.Series([1, 2, 3, 4]) + +# Pickup and delivery pairs (indices into order array) +# Order 0 (pickup) pairs with Order 1 (delivery) +# Order 2 (pickup) pairs with Order 3 (delivery) +pickup_indices = cudf.Series([0, 2]) +delivery_indices = cudf.Series([1, 3]) + +# Demand: positive for pickup, negative for delivery (must sum to 0 per pair) +demand = cudf.Series([10, -10, 15, -15], dtype="int32") +vehicle_capacity = cudf.Series([50, 50], dtype="int32") + +# Build model +dm = routing.DataModel( + n_locations=cost_matrix.shape[0], + n_fleet=n_fleet, + n_orders=n_orders +) + +dm.add_cost_matrix(cost_matrix) +dm.add_transit_time_matrix(transit_time_matrix) +dm.set_order_locations(order_locations) + +# Add capacity dimension +dm.add_capacity_dimension("load", demand, vehicle_capacity) + +# Set pickup and delivery constraints +dm.set_pickup_delivery_pairs(pickup_indices, delivery_indices) + +# Fleet setup +dm.set_vehicle_locations( + cudf.Series([0, 0]), # Start at depot + cudf.Series([0, 0]) # Return to depot +) + +# Solve +ss = routing.SolverSettings() +ss.set_time_limit(10) +solution = routing.Solve(dm, ss) + +print(f"Status: {solution.get_status()}") +if solution.get_status() == 0: + solution.display_routes() +``` + +## Minimal VRP (Quick Start) + +```python +import cudf +from cuopt import routing + +# Minimal 4-location problem +cost_matrix = cudf.DataFrame([ + [0, 10, 15, 20], + [10, 0, 12, 18], + [15, 12, 0, 10], + [20, 18, 10, 0], +], dtype="float32") + +dm = routing.DataModel(n_locations=4, n_fleet=1, n_orders=3) +dm.add_cost_matrix(cost_matrix) +dm.set_order_locations(cudf.Series([1, 2, 3])) + +solution = routing.Solve(dm, routing.SolverSettings()) + +if solution.get_status() == 0: + solution.display_routes() +``` + +## Multi-Depot VRP + +```python +import cudf +from cuopt import routing + +# 6 locations: 2 depots (0, 1) + 4 customers (2, 3, 4, 5) +cost_matrix = cudf.DataFrame([ + [0, 5, 10, 15, 20, 25], + [5, 0, 12, 8, 18, 22], + [10, 12, 0, 6, 14, 16], + [15, 8, 6, 0, 10, 12], + [20, 18, 14, 10, 0, 8], + [25, 22, 16, 12, 8, 0], +], dtype="float32") + +n_fleet = 2 + +dm = routing.DataModel(n_locations=6, n_fleet=n_fleet, n_orders=4) +dm.add_cost_matrix(cost_matrix) +dm.set_order_locations(cudf.Series([2, 3, 4, 5])) + +# Vehicle 0 starts/ends at depot 0, Vehicle 1 at depot 1 +dm.set_vehicle_locations( + cudf.Series([0, 1]), # start locations + cudf.Series([0, 1]) # end locations +) + +solution = routing.Solve(dm, routing.SolverSettings()) +if solution.get_status() == 0: + solution.display_routes() +``` + +--- + +## Additional References (tested in CI) + +For more complete examples, read these files: + +| Example | File | Description | +|---------|------|-------------| +| Basic Routing | `docs/cuopt/source/cuopt-server/examples/routing/examples/basic_routing_example.py` | Server-based routing | +| Initial Solution | `docs/cuopt/source/cuopt-server/examples/routing/examples/initial_solution_example.py` | Warm starting | +| Smoke Test | `docs/cuopt/source/cuopt-python/routing/examples/smoke_test_example.sh` | Quick validation | + +These examples are tested by CI and represent canonical usage. + +**Note:** The Python routing API documentation is in `python/cuopt/cuopt/routing/vehicle_routing.py` (docstrings). diff --git a/.github/skills/cuopt-routing/resources/server_examples.md b/.github/skills/cuopt-routing/resources/server_examples.md new file mode 100644 index 0000000000..06d03dbe77 --- /dev/null +++ b/.github/skills/cuopt-routing/resources/server_examples.md @@ -0,0 +1,204 @@ +# Routing: REST Server Examples + +## Start the Server + +```bash +# Start server +python -m cuopt_server.cuopt_service --ip 0.0.0.0 --port 8000 & + +# Wait and verify +sleep 5 +curl -s http://localhost:8000/cuopt/health +``` + +## Basic VRP (curl) + +```bash +REQID=$(curl -s -X POST "http://localhost:8000/cuopt/request" \ + -H "Content-Type: application/json" \ + -H "CLIENT-VERSION: custom" \ + -d '{ + "cost_matrix_data": { + "data": {"0": [[0,10,15,20],[10,0,12,18],[15,12,0,10],[20,18,10,0]]} + }, + "travel_time_matrix_data": { + "data": {"0": [[0,10,15,20],[10,0,12,18],[15,12,0,10],[20,18,10,0]]} + }, + "task_data": { + "task_locations": [1, 2, 3], + "demand": [[10, 15, 20]], + "task_time_windows": [[0, 100], [10, 80], [20, 90]], + "service_times": [5, 5, 5] + }, + "fleet_data": { + "vehicle_locations": [[0, 0], [0, 0]], + "capacities": [[50, 50]], + "vehicle_time_windows": [[0, 200], [0, 200]] + }, + "solver_config": { + "time_limit": 5 + } + }' | jq -r '.reqId') + +echo "Request ID: $REQID" + +# Poll for solution +sleep 2 +curl -s "http://localhost:8000/cuopt/solution/$REQID" \ + -H "Content-Type: application/json" \ + -H "CLIENT-VERSION: custom" | jq . +``` + +## VRP with Time Windows (Python requests) + +```python +import requests +import time + +SERVER = "http://localhost:8000" +HEADERS = {"Content-Type": "application/json", "CLIENT-VERSION": "custom"} + +payload = { + "cost_matrix_data": { + "data": { + "0": [ + [0, 10, 15, 20, 25], + [10, 0, 12, 18, 22], + [15, 12, 0, 10, 15], + [20, 18, 10, 0, 8], + [25, 22, 15, 8, 0] + ] + } + }, + "travel_time_matrix_data": { + "data": { + "0": [ + [0, 10, 15, 20, 25], + [10, 0, 12, 18, 22], + [15, 12, 0, 10, 15], + [20, 18, 10, 0, 8], + [25, 22, 15, 8, 0] + ] + } + }, + "task_data": { + "task_locations": [1, 2, 3, 4], + "demand": [[20, 30, 25, 15]], + "task_time_windows": [[0, 50], [10, 60], [20, 70], [0, 80]], + "service_times": [5, 5, 5, 5] + }, + "fleet_data": { + "vehicle_locations": [[0, 0], [0, 0]], + "capacities": [[100, 100]], + "vehicle_time_windows": [[0, 200], [0, 200]] + }, + "solver_config": { + "time_limit": 10 + } +} + +# Submit request +response = requests.post(f"{SERVER}/cuopt/request", json=payload, headers=HEADERS) +response.raise_for_status() +req_id = response.json()["reqId"] +print(f"Request submitted: {req_id}") + +# Poll for solution +for attempt in range(30): + response = requests.get(f"{SERVER}/cuopt/solution/{req_id}", headers=HEADERS) + result = response.json() + + if "response" in result: + solver_response = result["response"].get("solver_response", {}) + print(f"\nSolution found!") + print(f"Status: {solver_response.get('status', 'N/A')}") + print(f"Cost: {solver_response.get('solution_cost', 'N/A')}") + + if "vehicle_data" in solver_response: + for vid, vdata in solver_response["vehicle_data"].items(): + route = vdata.get("route", []) + print(f"Vehicle {vid}: {' -> '.join(map(str, route))}") + break + else: + print(f"Waiting... (attempt {attempt + 1})") + time.sleep(1) +``` + +## Pickup and Delivery (curl) + +```bash +REQID=$(curl -s -X POST "http://localhost:8000/cuopt/request" \ + -H "Content-Type: application/json" \ + -H "CLIENT-VERSION: custom" \ + -d '{ + "cost_matrix_data": { + "data": {"0": [[0,10,20,30,40],[10,0,15,25,35],[20,15,0,10,20],[30,25,10,0,15],[40,35,20,15,0]]} + }, + "travel_time_matrix_data": { + "data": {"0": [[0,10,20,30,40],[10,0,15,25,35],[20,15,0,10,20],[30,25,10,0,15],[40,35,20,15,0]]} + }, + "task_data": { + "task_locations": [1, 2, 3, 4], + "demand": [[10, -10, 15, -15]], + "pickup_and_delivery_pairs": [[0, 1], [2, 3]] + }, + "fleet_data": { + "vehicle_locations": [[0, 0]], + "capacities": [[50]] + }, + "solver_config": { + "time_limit": 10 + } + }' | jq -r '.reqId') + +echo "Request ID: $REQID" + +# Poll for solution +sleep 2 +curl -s "http://localhost:8000/cuopt/solution/$REQID" \ + -H "Content-Type: application/json" \ + -H "CLIENT-VERSION: custom" | jq . +``` + +## Terminology Reference + +| Python API | REST Server API | +|------------|-----------------| +| `order_locations` | `task_locations` | +| `set_order_time_windows()` | `task_time_windows` | +| `set_order_service_times()` | `service_times` | +| `add_transit_time_matrix()` | `travel_time_matrix_data` | +| `set_pickup_delivery_pairs()` | `pickup_and_delivery_pairs` | + +## Common Payload Mistakes + +```json +// ❌ WRONG field name +"transit_time_matrix_data": {...} + +// ✅ CORRECT +"travel_time_matrix_data": {...} +``` + +```json +// ❌ WRONG capacity format (per vehicle) +"capacities": [[50], [50]] + +// ✅ CORRECT (per dimension across vehicles) +"capacities": [[50, 50]] +``` + +--- + +## Additional References (tested in CI) + +For more complete examples, read these files: + +| Example | File | Description | +|---------|------|-------------| +| Basic Routing (Python) | `docs/cuopt/source/cuopt-server/examples/routing/examples/basic_routing_example.py` | VRP via REST | +| Basic Routing (curl) | `docs/cuopt/source/cuopt-server/examples/routing/examples/basic_routing_example.sh` | Shell script | +| Initial Solution | `docs/cuopt/source/cuopt-server/examples/routing/examples/initial_solution_example.py` | Warm starting | +| Initial Solution (curl) | `docs/cuopt/source/cuopt-server/examples/routing/examples/initial_solution_example.sh` | Warm start shell | + +These examples are tested by CI (`ci/test_doc_examples.sh`) and represent canonical usage. diff --git a/.github/skills/cuopt-server/SKILL.md b/.github/skills/cuopt-server/SKILL.md new file mode 100644 index 0000000000..e118a0ba43 --- /dev/null +++ b/.github/skills/cuopt-server/SKILL.md @@ -0,0 +1,356 @@ +--- +name: cuopt-server +description: Deploy and integrate cuOpt REST server for production use. Use when the user asks about REST API, HTTP endpoints, deployment, curl requests, microservices, async solving, or server payloads. +--- + +# cuOpt Server Skill + +> **Prerequisites**: Read `cuopt-user-rules/SKILL.md` first for behavior rules. + +Deploy and use the cuOpt REST server for production optimization workloads. + +## Before You Start: Required Questions + +**Ask these if not already clear:** + +1. **Problem type?** + - Routing (VRP/TSP/PDP)? + - LP/MILP? + - (Note: QP not supported via REST) + +2. **Deployment target?** + - Local development? + - Docker/Kubernetes? + - Cloud service? + +3. **Client preference?** + - curl (quick testing) + - Python requests + - cuopt-sh-client library + +## Server Capabilities + +| Problem Type | REST Support | +|--------------|:------------:| +| Routing | ✓ | +| LP | ✓ | +| MILP | ✓ | +| QP | ✗ | + +## Starting the Server + +### Direct (Development) + +```bash +python -m cuopt_server.cuopt_service --ip 0.0.0.0 --port 8000 +``` + +### Docker (Production) + +```bash +docker run --gpus all -d \ + -p 8000:8000 \ + -e CUOPT_SERVER_PORT=8000 \ + --name cuopt-server \ + nvidia/cuopt:latest-cuda12.9-py3.13 +``` + +### Verify Running + +```bash +curl http://localhost:8000/cuopt/health +# Expected: {"status": "healthy"} +``` + +## API Endpoints + +| Endpoint | Method | Purpose | +|----------|--------|---------| +| `/cuopt/health` | GET | Health check | +| `/cuopt/request` | POST | Submit optimization request | +| `/cuopt/solution/{reqId}` | GET | Get solution by request ID | +| `/cuopt.yaml` | GET | OpenAPI specification | +| `/cuopt/docs` | GET | Swagger UI | + +## Workflow + +1. **POST** problem to `/cuopt/request` → get `reqId` +2. **Poll** `/cuopt/solution/{reqId}` until solution ready +3. **Parse** response + +## Routing Request Example + +### curl + +```bash +REQID=$(curl -s -X POST "http://localhost:8000/cuopt/request" \ + -H "Content-Type: application/json" \ + -H "CLIENT-VERSION: custom" \ + -d '{ + "cost_matrix_data": { + "data": {"0": [[0,10,15],[10,0,12],[15,12,0]]} + }, + "travel_time_matrix_data": { + "data": {"0": [[0,10,15],[10,0,12],[15,12,0]]} + }, + "task_data": { + "task_locations": [1, 2], + "demand": [[10, 20]], + "task_time_windows": [[0, 100], [0, 100]], + "service_times": [5, 5] + }, + "fleet_data": { + "vehicle_locations": [[0, 0]], + "capacities": [[50]], + "vehicle_time_windows": [[0, 200]] + }, + "solver_config": {"time_limit": 5} + }' | jq -r '.reqId') + +echo "Request ID: $REQID" + +# Poll for solution +sleep 2 +curl -s "http://localhost:8000/cuopt/solution/$REQID" \ + -H "CLIENT-VERSION: custom" | jq . +``` + +### Python + +```python +import requests +import time + +SERVER = "http://localhost:8000" +HEADERS = {"Content-Type": "application/json", "CLIENT-VERSION": "custom"} + +payload = { + "cost_matrix_data": { + "data": {"0": [[0,10,15],[10,0,12],[15,12,0]]} + }, + "travel_time_matrix_data": { + "data": {"0": [[0,10,15],[10,0,12],[15,12,0]]} + }, + "task_data": { + "task_locations": [1, 2], + "demand": [[10, 20]], + "task_time_windows": [[0, 100], [0, 100]], # optional + "service_times": [5, 5] # optional + }, + "fleet_data": { + "vehicle_locations": [[0, 0]], + "capacities": [[50]], + "vehicle_time_windows": [[0, 200]] # optional + }, + "solver_config": {"time_limit": 5} +} + +# Submit +resp = requests.post(f"{SERVER}/cuopt/request", json=payload, headers=HEADERS) +req_id = resp.json()["reqId"] + +# Poll +for _ in range(30): + resp = requests.get(f"{SERVER}/cuopt/solution/{req_id}", headers=HEADERS) + result = resp.json() + if "response" in result: + print(result["response"]["solver_response"]) + break + time.sleep(1) +``` + +## LP/MILP Request Example + +```bash +curl -s -X POST "http://localhost:8000/cuopt/request" \ + -H "Content-Type: application/json" \ + -H "CLIENT-VERSION: custom" \ + -d '{ + "csr_constraint_matrix": { + "offsets": [0, 2, 4], + "indices": [0, 1, 0, 1], + "values": [2.0, 3.0, 4.0, 2.0] + }, + "constraint_bounds": { + "upper_bounds": [240.0, 200.0], + "lower_bounds": ["ninf", "ninf"] + }, + "objective_data": { + "coefficients": [40.0, 30.0], + "scalability_factor": 1.0, + "offset": 0.0 + }, + "variable_bounds": { + "upper_bounds": ["inf", "inf"], + "lower_bounds": [0.0, 0.0] + }, + "maximize": true, + "solver_config": {"time_limit": 60} + }' +``` + +## Terminology: REST vs Python API + +**CRITICAL:** REST API uses different terminology than Python API. + +| Concept | Python API | REST API | +|---------|------------|----------| +| Orders/Jobs | `order_locations` | `task_locations` | +| Time windows | `set_order_time_windows()` | `task_time_windows` | +| Service times | `set_order_service_times()` | `service_times` | +| Transit matrix | `add_transit_time_matrix()` | `travel_time_matrix_data` | + +## Common Payload Mistakes + +### Wrong field names + +```json +// ❌ WRONG +"transit_time_matrix_data": {...} + +// ✅ CORRECT +"travel_time_matrix_data": {...} +``` + +### Wrong capacity format + +```json +// ❌ WRONG - per vehicle +"capacities": [[50], [50]] + +// ✅ CORRECT - per dimension across all vehicles +"capacities": [[50, 50]] +``` + +### Missing required fields + +Routing requires at minimum: +- `cost_matrix_data` +- `task_data.task_locations` +- `fleet_data.vehicle_locations` +- `fleet_data.capacities` + +## Response Structure + +### Routing Success + +```json +{ + "reqId": "abc123", + "response": { + "solver_response": { + "status": 0, + "solution_cost": 45.0, + "vehicle_data": { + "0": {"route": [0, 1, 2, 0], "arrival_times": [...]} + } + } + } +} +``` + +### LP/MILP Success + +```json +{ + "reqId": "abc123", + "response": { + "status": "Optimal", + "objective_value": 1600.0, + "primal_solution": [30.0, 60.0] + } +} +``` + +## Error Handling + +### 422 Validation Error + +Check the error message for field issues: +```bash +curl ... | jq '.error' +``` + +Compare against OpenAPI spec at `/cuopt.yaml` + +### 500 Server Error + +- Check server logs +- Capture `reqId` for debugging +- Try with smaller problem + +### Polling Returns Empty + +- Solution still computing - keep polling +- Check `solver_config.time_limit` + +## Server Configuration + +### Environment Variables + +```bash +CUOPT_SERVER_PORT=8000 +CUOPT_SERVER_HOST=0.0.0.0 +``` + +### Command Line Options + +```bash +python -m cuopt_server.cuopt_service \ + --ip 0.0.0.0 \ + --port 8000 \ + --workers 4 +``` + +## Production Considerations + +### Health Checks + +```bash +# Kubernetes liveness probe +curl -f http://localhost:8000/cuopt/health + +# Readiness check +curl -f http://localhost:8000/cuopt/health +``` + +### Resource Limits + +```yaml +# Kubernetes example +resources: + limits: + nvidia.com/gpu: 1 + memory: "32Gi" + requests: + memory: "16Gi" +``` + +### Scaling + +- GPU is the bottleneck - one server per GPU +- Use load balancer for multiple GPUs +- Queue requests to avoid overwhelming + +## OpenAPI Specification + +Full API spec available at: +- Runtime: `http://localhost:8000/cuopt.yaml` +- Source: `docs/cuopt/source/cuopt_spec.yaml` +- Swagger UI: `http://localhost:8000/cuopt/docs` + +## Examples + +See `resources/` for complete examples: +- [Routing examples](resources/routing_examples.md) — VRP, PDP via REST +- [LP/MILP examples](resources/lp_milp_examples.md) — Linear programming via REST + +## When to Escalate + +Switch to **cuopt-debugging** if: +- Consistent 5xx errors +- Unexpected solution results + +Switch to **cuopt-developer** if: +- Need to modify server behavior +- Need new endpoints diff --git a/.github/skills/cuopt-server/resources/lp_milp_examples.md b/.github/skills/cuopt-server/resources/lp_milp_examples.md new file mode 100644 index 0000000000..107bb2490b --- /dev/null +++ b/.github/skills/cuopt-server/resources/lp_milp_examples.md @@ -0,0 +1,176 @@ +# Server: LP/MILP Examples + +## LP Request (curl) + +```bash +# maximize 40*x + 30*y +# s.t. 2x + 3y <= 240 +# 4x + 2y <= 200 + +REQID=$(curl -s -X POST "http://localhost:8000/cuopt/request" \ + -H "Content-Type: application/json" \ + -H "CLIENT-VERSION: custom" \ + -d '{ + "csr_constraint_matrix": { + "offsets": [0, 2, 4], + "indices": [0, 1, 0, 1], + "values": [2.0, 3.0, 4.0, 2.0] + }, + "constraint_bounds": { + "upper_bounds": [240.0, 200.0], + "lower_bounds": ["ninf", "ninf"] + }, + "objective_data": { + "coefficients": [40.0, 30.0], + "scalability_factor": 1.0, + "offset": 0.0 + }, + "variable_bounds": { + "upper_bounds": ["inf", "inf"], + "lower_bounds": [0.0, 0.0] + }, + "maximize": true, + "solver_config": { + "time_limit": 60 + } + }' | jq -r '.reqId') + +sleep 2 +curl -s "http://localhost:8000/cuopt/solution/$REQID" -H "CLIENT-VERSION: custom" | jq . +``` + +## MILP Request (curl) + +```bash +# Submit MILP request and capture reqId +REQID=$(curl -s -X POST "http://localhost:8000/cuopt/request" \ + -H "Content-Type: application/json" \ + -H "CLIENT-VERSION: custom" \ + -d '{ + "csr_constraint_matrix": { + "offsets": [0, 2, 4], + "indices": [0, 1, 0, 1], + "values": [2.0, 3.0, 4.0, 2.0] + }, + "constraint_bounds": { + "upper_bounds": [240.0, 200.0], + "lower_bounds": ["ninf", "ninf"] + }, + "objective_data": { + "coefficients": [40.0, 30.0] + }, + "variable_bounds": { + "upper_bounds": ["inf", "inf"], + "lower_bounds": [0.0, 0.0] + }, + "variable_types": ["integer", "continuous"], + "maximize": true, + "solver_config": { + "time_limit": 120, + "tolerances": { + "mip_relative_gap": 0.01 + } + } + }' | jq -r '.reqId') +# Note: objective_data also supports optional "scalability_factor" and "offset" fields + +# Poll for solution (MILP may take longer than LP) +sleep 3 +curl -s "http://localhost:8000/cuopt/solution/$REQID" -H "CLIENT-VERSION: custom" | jq . +``` + +## LP Request (Python) + +```python +import requests +import time + +SERVER = "http://localhost:8000" +HEADERS = {"Content-Type": "application/json", "CLIENT-VERSION": "custom"} + +payload = { + "csr_constraint_matrix": { + "offsets": [0, 2, 4], + "indices": [0, 1, 0, 1], + "values": [2.0, 3.0, 4.0, 2.0] + }, + "constraint_bounds": { + "upper_bounds": [240.0, 200.0], + "lower_bounds": ["ninf", "ninf"] + }, + "objective_data": { + "coefficients": [40.0, 30.0] + }, + "variable_bounds": { + "upper_bounds": ["inf", "inf"], + "lower_bounds": [0.0, 0.0] + }, + "maximize": True, + "solver_config": { + "time_limit": 60 + } +} + +# Submit +response = requests.post(f"{SERVER}/cuopt/request", json=payload, headers=HEADERS) +req_id = response.json()["reqId"] +print(f"Submitted: {req_id}") + +# Poll for solution +for _ in range(30): + response = requests.get(f"{SERVER}/cuopt/solution/{req_id}", headers=HEADERS) + result = response.json() + + if "response" in result: + print(f"Status: {result['response'].get('status')}") + print(f"Objective: {result['response'].get('objective_value')}") + print(f"Solution: {result['response'].get('primal_solution')}") + break + time.sleep(1) +``` + +## CSR Matrix Format + +``` +Matrix: [2, 3] (row 0: 2*x0 + 3*x1) + [4, 2] (row 1: 4*x0 + 2*x1) + +CSR format: + offsets: [0, 2, 4] # Row pointers (n_rows + 1) + indices: [0, 1, 0, 1] # Column indices + values: [2.0, 3.0, 4.0, 2.0] # Non-zero values +``` + +## Special Values + +```json +{ + "constraint_bounds": { + "lower_bounds": ["ninf", "ninf"], + "upper_bounds": [100.0, "inf"] + } +} +``` + +## Variable Types + +- `"continuous"` - real-valued +- `"integer"` - integer-valued +- `"binary"` - 0 or 1 only + +--- + +## Additional References (tested in CI) + +For more complete examples, read these files: + +| Example | File | +|---------|------| +| Basic LP (Python) | `docs/cuopt/source/cuopt-server/examples/lp/examples/basic_lp_example.py` | +| Basic LP (curl) | `docs/cuopt/source/cuopt-server/examples/lp/examples/basic_lp_example.sh` | +| MPS File Input | `docs/cuopt/source/cuopt-server/examples/lp/examples/mps_file_example.py` | +| Warmstart | `docs/cuopt/source/cuopt-server/examples/lp/examples/warmstart_example.py` | +| Basic MILP | `docs/cuopt/source/cuopt-server/examples/milp/examples/basic_milp_example.py` | +| Incumbent Callback | `docs/cuopt/source/cuopt-server/examples/milp/examples/incumbent_callback_example.py` | + +These examples are tested by CI (`ci/test_doc_examples.sh`). diff --git a/.github/skills/cuopt-server/resources/routing_examples.md b/.github/skills/cuopt-server/resources/routing_examples.md new file mode 100644 index 0000000000..9caf7e67dd --- /dev/null +++ b/.github/skills/cuopt-server/resources/routing_examples.md @@ -0,0 +1,160 @@ +# Server: Routing Examples + +## Start Server + +```bash +python -m cuopt_server.cuopt_service --ip 0.0.0.0 --port 8000 & +sleep 5 +curl http://localhost:8000/cuopt/health +``` + +> **Note:** Using `--ip 0.0.0.0` binds to all interfaces for development convenience; use `--ip 127.0.0.1` or a specific interface in production or untrusted networks. + +## Basic VRP (curl) + +```bash +REQID=$(curl -s -X POST "http://localhost:8000/cuopt/request" \ + -H "Content-Type: application/json" \ + -H "CLIENT-VERSION: custom" \ + -d '{ + "cost_matrix_data": { + "data": {"0": [[0,10,15,20],[10,0,12,18],[15,12,0,10],[20,18,10,0]]} + }, + "travel_time_matrix_data": { + "data": {"0": [[0,10,15,20],[10,0,12,18],[15,12,0,10],[20,18,10,0]]} + }, + "task_data": { + "task_locations": [1, 2, 3], + "demand": [[10, 15, 20]], + "service_times": [5, 5, 5] + }, + "fleet_data": { + "vehicle_locations": [[0, 0], [0, 0]], + "capacities": [[50, 50]] + }, + "solver_config": {"time_limit": 5} + }' | jq -r '.reqId') + +curl -s "http://localhost:8000/cuopt/solution/$REQID" -H "CLIENT-VERSION: custom" | jq . +``` + +## VRP with Time Windows (Python) + +```python +import requests +import time + +SERVER = "http://localhost:8000" +HEADERS = {"Content-Type": "application/json", "CLIENT-VERSION": "custom"} + +payload = { + "cost_matrix_data": { + "data": {"0": [[0,10,15,20,25],[10,0,12,18,22],[15,12,0,10,15],[20,18,10,0,8],[25,22,15,8,0]]} + }, + "travel_time_matrix_data": { + "data": {"0": [[0,10,15,20,25],[10,0,12,18,22],[15,12,0,10,15],[20,18,10,0,8],[25,22,15,8,0]]} + }, + "task_data": { + "task_locations": [1, 2, 3, 4], + "demand": [[20, 30, 25, 15]], + "task_time_windows": [[0, 50], [10, 60], [20, 70], [0, 80]], + "service_times": [5, 5, 5, 5] + }, + "fleet_data": { + "vehicle_locations": [[0, 0], [0, 0]], + "capacities": [[100, 100]], + "vehicle_time_windows": [[0, 200], [0, 200]] + }, + "solver_config": { + "time_limit": 10 + } +} + +# Submit +response = requests.post(f"{SERVER}/cuopt/request", json=payload, headers=HEADERS) +req_id = response.json()["reqId"] +print(f"Submitted: {req_id}") + +# Poll for solution +for attempt in range(30): + response = requests.get(f"{SERVER}/cuopt/solution/{req_id}", headers=HEADERS) + result = response.json() + + if "response" in result: + solver_response = result["response"].get("solver_response", {}) + print(f"Status: {solver_response.get('status')}") + print(f"Cost: {solver_response.get('solution_cost')}") + if "vehicle_data" in solver_response: + for vid, vdata in solver_response["vehicle_data"].items(): + print(f"Vehicle {vid}: {vdata.get('route', [])}") + break + time.sleep(1) +``` + +## Pickup and Delivery (curl) + +```bash +curl -s -X POST "http://localhost:8000/cuopt/request" \ + -H "Content-Type: application/json" \ + -H "CLIENT-VERSION: custom" \ + -d '{ + "cost_matrix_data": { + "data": {"0": [[0,10,20,30,40],[10,0,15,25,35],[20,15,0,10,20],[30,25,10,0,15],[40,35,20,15,0]]} + }, + "travel_time_matrix_data": { + "data": {"0": [[0,10,20,30,40],[10,0,15,25,35],[20,15,0,10,20],[30,25,10,0,15],[40,35,20,15,0]]} + }, + "task_data": { + "task_locations": [1, 2, 3, 4], + "demand": [[10, -10, 15, -15]], + "pickup_and_delivery_pairs": [[0, 1], [2, 3]] + }, + "fleet_data": { + "vehicle_locations": [[0, 0]], + "capacities": [[50]] + }, + "solver_config": {"time_limit": 10} + }' | jq . +``` + +## Terminology: Python vs REST + +| Python API | REST Server | +|------------|-------------| +| `order_locations` | `task_locations` | +| `set_order_time_windows()` | `task_time_windows` | +| `set_order_service_times()` | `service_times` | +| `add_transit_time_matrix()` | `travel_time_matrix_data` | +| `set_pickup_delivery_pairs()` | `pickup_and_delivery_pairs` | + +## Common Mistakes + +```json +// ❌ WRONG field name +"transit_time_matrix_data": {...} + +// ✅ CORRECT +"travel_time_matrix_data": {...} +``` + +```json +// ❌ WRONG capacity format (per vehicle) +"capacities": [[50], [50]] + +// ✅ CORRECT (per dimension across vehicles) +"capacities": [[50, 50]] +``` + +--- + +## Additional References (tested in CI) + +For more complete examples, read these files: + +| Example | File | +|---------|------| +| Basic Routing (Python) | `docs/cuopt/source/cuopt-server/examples/routing/examples/basic_routing_example.py` | +| Basic Routing (curl) | `docs/cuopt/source/cuopt-server/examples/routing/examples/basic_routing_example.sh` | +| Initial Solution | `docs/cuopt/source/cuopt-server/examples/routing/examples/initial_solution_example.py` | + +These examples are tested by CI (`ci/test_doc_examples.sh`). diff --git a/.github/skills/cuopt-user-rules/SKILL.md b/.github/skills/cuopt-user-rules/SKILL.md new file mode 100644 index 0000000000..a6c870c10d --- /dev/null +++ b/.github/skills/cuopt-user-rules/SKILL.md @@ -0,0 +1,211 @@ +--- +name: cuopt-user-rules +description: Base behavior rules for using NVIDIA cuOpt. Read this FIRST before any cuOpt user task (routing, LP/MILP, QP, debugging, installation, server). Covers handling incomplete questions, clarifying data requirements, verifying understanding, and running commands safely. +--- + +# cuOpt User Rules + +**Read this before using any cuOpt skill.** These rules ensure you help users effectively and safely. + +--- + +## 1. Ask Before Assuming + +**Always clarify ambiguous requirements before implementing:** + +- What interface? (Python API / REST Server / C API / CLI) +- What problem type? (Routing / LP / MILP / QP) +- What constraints matter? (time windows, capacities, etc.) +- What output format? (solution values, routes, visualization) + +**Skip asking only if:** +- User explicitly stated the requirement +- Context makes it unambiguous (e.g., user shows Python code) + +--- + +## 2. Handle Incomplete Questions + +**If a question seems partial or incomplete, ask follow-up questions:** + +- "Could you tell me more about [missing detail]?" +- "What specifically would you like to achieve with this?" +- "Are there any constraints or requirements I should know about?" + +**Common missing information to probe for:** +- Problem size (number of vehicles, locations, variables, constraints) +- Specific constraints (time windows, capacities, precedence) +- Performance requirements (time limits, solution quality) +- Integration context (existing codebase, deployment environment) + +**Don't guess — ask.** A brief clarifying question saves time vs. solving the wrong problem. + +--- + +## 3. Clarify Data Requirements + +**Before generating examples, ask about data:** + +1. **Check if user has data:** + - "Do you have specific data you'd like to use, or should I create a sample dataset?" + - "Can you share the format of your input data?" + +2. **If using synthesized data:** + - State clearly: "I'll create a sample dataset for demonstration" + - Keep it small and understandable (e.g., 5-10 locations, 2-3 vehicles) + - Make values realistic and meaningful + +3. **Always document what you used:** + ``` + "For this example I'm using: + - [X] locations/variables/constraints + - [Key assumptions: e.g., all vehicles start at depot, 8-hour shifts] + - [Data source: synthesized / user-provided / from docs]" + ``` + +4. **State assumptions explicitly:** + - "I'm assuming [X] — let me know if this differs from your scenario" + - List any default values or simplifications made + +--- + +## 4. MUST Verify Understanding + +**Before writing substantial code, you MUST confirm your understanding:** + +``` +"Let me confirm I understand: +- Problem: [restate in your words] +- Constraints: [list them] +- Objective: [minimize/maximize what] +- Interface: [Python/REST/C/CLI] +Is this correct?" +``` + +--- + +## 5. Follow Requirements Exactly + +- Use the **exact** variable names, formats, and structures the user specifies +- Don't add features the user didn't ask for +- Don't change the problem formulation unless asked +- If user provides partial code, extend it—don't rewrite from scratch + +--- + +## 6. Read Examples First + +Before generating code, **read the canonical example** for that problem type: + +| Problem | Example Location | +|---------|------------------| +| Routing | `docs/cuopt/source/cuopt-python/routing/examples/` | +| LP/MILP | `docs/cuopt/source/cuopt-python/lp-qp-milp/examples/` | +| QP | `docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_qp_example.py` | +| Server | `docs/cuopt/source/cuopt_spec.yaml` (OpenAPI) | +| C API | `docs/cuopt/source/cuopt-c/lp-qp-milp/examples/` | + +**Don't invent API patterns.** Copy from examples. + +--- + +## 7. Check Results + +After providing a solution, guide the user to verify: + +- **Status check**: Is it `Optimal` / `FeasibleFound` / `SUCCESS`? +- **Constraint satisfaction**: Are all constraints met? +- **Objective value**: Is it reasonable for the problem? + +Provide diagnostic code snippets when helpful. + +--- + +## 8. Check Environment First + +**Before writing code or suggesting installation, verify the user's setup:** + +1. **Ask how they access cuOpt:** + - "Do you have cuOpt installed? If so, which interface?" + - "What environment are you using? (local GPU, cloud, Docker, server, etc.)" + +2. **Different packages for different interfaces:** + + | Interface | Package | Check | + |-----------|---------|-------| + | Python API | `cuopt` (pip/conda) | `import cuopt` | + | C API | `libcuopt` (conda/system) | `find libcuopt.so` or header check | + | REST Server | `cuopt-server` or Docker | `curl /cuopt/health` | + | CLI | `cuopt` package includes CLI | `cuopt_cli --help` | + + **Note:** `libcuopt` (C library) installed via conda is NOT available through Python import — they are separate packages. + +3. **If not installed, ask how they want to access:** + - "Would you like help installing cuOpt, or do you have access another way?" + - Options: pip, conda, Docker, cloud instance, existing remote server + +4. **Never assume installation is needed** — the user may: + - Already have it installed + - Be connecting to a remote server + - Prefer a specific installation method + - Only need the C library (not Python) + +5. **Ask before running any verification commands:** + ```python + # Python API check - ask first + import cuopt + print(cuopt.__version__) + ``` + ```bash + # C API check - ask first + find ${CONDA_PREFIX} -name "libcuopt.so" + ``` + ```bash + # Server check - ask first + curl http://localhost:8000/cuopt/health + ``` + +--- + +## 9. Ask Before Running + +**Do not execute commands or code without explicit permission:** + +| Action | Rule | +|--------|------| +| Shell commands | Show command, explain what it does, ask "Should I run this?" | +| Package installs | **Never** run `pip`, `conda`, `apt` without asking first | +| Examples/scripts | Show the code first, ask "Would you like me to run this?" | +| File writes | Explain what will change, ask before writing | + +**Exceptions (okay without asking):** +- Read-only commands the user explicitly requested +- Commands the user just provided and asked you to run + +--- + +## 10. No Privileged Operations + +**Never do these without explicit user request AND confirmation:** + +- Use `sudo` or run as root +- Modify system files or configurations +- Add package repositories or keys +- Change firewall, network, or driver settings +- Write files outside the workspace + +--- + +## Resources + +### Documentation +- [cuOpt User Guide](https://docs.nvidia.com/cuopt/user-guide/latest/introduction.html) +- [API Reference](https://docs.nvidia.com/cuopt/user-guide/latest/api.html) + +### Examples +- [cuopt-examples repo](https://github.com/NVIDIA/cuopt-examples) +- [Google Colab notebooks](https://colab.research.google.com/github/nvidia/cuopt-examples/) + +### Support +- [NVIDIA Developer Forums](https://forums.developer.nvidia.com/c/ai-data-science/nvidia-cuopt/514) +- [GitHub Issues](https://github.com/NVIDIA/cuopt/issues) diff --git a/AGENTS.md b/AGENTS.md index 1cd7c9e430..f4e47cde2b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,8 @@ -# AGENTS.md (shim) +# AGENTS.md -The canonical AI-agent entrypoint for this repo is: +AI-agent skills for this repo are located at: -- `.github/AGENTS.md` +- **Entry point**: `.github/AGENTS.md` +- **Skills**: `.github/skills/` -If you are a coding agent, start there. +If you are a coding agent, start at `.github/AGENTS.md`.