From f048d7ddcac0dbc55067fc6847ac8ce1d835ad25 Mon Sep 17 00:00:00 2001 From: legraina Date: Thu, 11 Jun 2026 16:27:15 +0200 Subject: [PATCH 01/10] docs: add GitHub Pages documentation site and AGENT.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/ — full Jekyll (just-the-docs) documentation site: * index.md — overview, quick examples (C++ and Python) * installation.md — FetchContent, cmake options, pip install * cpp/ — concepts, quick start, full C++ API reference * python/ — quick start, full Python API reference * advanced/ — algorithms guide, column-generation patterns - AGENT.md — self-contained guide for AI agents: data model, all import paths, essential API patterns (Python + C++), resource function quick-reference tables, build instructions, common pitfalls - .github/workflows/pages.yml — auto-deploy to GitHub Pages on push - .markdownlint.json — relax line-length/heading rules for docs - .github/workflows/ci-full.yml fixes: * allow-prereleases: true for Python 3.14 * -DCMAKE_CXX_CLANG_TIDY= to disable clang-tidy in test matrix * macOS: export CC/CXX to brew LLVM so CMake uses the right compiler Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci-full.yml | 13 +- .github/workflows/pages.yml | 51 ++++ .markdownlint.json | 15 ++ AGENT.md | 388 +++++++++++++++++++++++++++++ docs/Gemfile | 5 + docs/_config.yml | 29 +++ docs/advanced/algorithms.md | 99 ++++++++ docs/advanced/column-generation.md | 181 ++++++++++++++ docs/advanced/index.md | 7 + docs/cpp/api.md | 311 +++++++++++++++++++++++ docs/cpp/concepts.md | 202 +++++++++++++++ docs/cpp/index.md | 49 ++++ docs/cpp/quickstart.md | 152 +++++++++++ docs/index.md | 129 ++++++++++ docs/python/api.md | 328 ++++++++++++++++++++++++ docs/python/index.md | 45 ++++ docs/python/quickstart.md | 186 ++++++++++++++ 17 files changed, 2188 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/pages.yml create mode 100644 .markdownlint.json create mode 100644 AGENT.md create mode 100644 docs/Gemfile create mode 100644 docs/_config.yml create mode 100644 docs/advanced/algorithms.md create mode 100644 docs/advanced/column-generation.md create mode 100644 docs/advanced/index.md create mode 100644 docs/cpp/api.md create mode 100644 docs/cpp/concepts.md create mode 100644 docs/cpp/index.md create mode 100644 docs/cpp/quickstart.md create mode 100644 docs/index.md create mode 100644 docs/python/api.md create mode 100644 docs/python/index.md create mode 100644 docs/python/quickstart.md diff --git a/.github/workflows/ci-full.yml b/.github/workflows/ci-full.yml index 89772d77..976c4ab3 100644 --- a/.github/workflows/ci-full.yml +++ b/.github/workflows/ci-full.yml @@ -52,14 +52,23 @@ jobs: if: runner.os == 'macOS' run: | brew install llvm - echo "$(brew --prefix llvm)/bin" >> $GITHUB_PATH + LLVM_PREFIX="$(brew --prefix llvm)" + echo "${LLVM_PREFIX}/bin" >> "$GITHUB_PATH" + echo "CC=${LLVM_PREFIX}/bin/clang" >> "$GITHUB_ENV" + echo "CXX=${LLVM_PREFIX}/bin/clang++" >> "$GITHUB_ENV" - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: Configure - run: cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} -DUSE_PYTHON=ON -DUSE_TESTS=ON + run: | + cmake -B build \ + -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} \ + -DUSE_PYTHON=ON \ + -DUSE_TESTS=ON \ + "-DCMAKE_CXX_CLANG_TIDY=" - name: Build run: cmake --build build --config ${{ matrix.build-type }} --parallel diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 00000000..1916cd95 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,51 @@ +name: GitHub Pages + +on: + push: + branches: [main] + paths: + - 'docs/**' + - 'AGENT.md' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/configure-pages@v5 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + bundler-cache: true + working-directory: docs + + - name: Build with Jekyll + run: bundle exec jekyll build --source docs --destination _site + env: + JEKYLL_ENV: production + + - uses: actions/upload-pages-artifact@v3 + with: + path: _site + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 00000000..036aca62 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,15 @@ +{ + "default": true, + "MD013": { + "line_length": 120, + "code_block_line_length": 200, + "tables": false + }, + "MD024": { + "siblings_only": true + }, + "MD025": { + "front_matter_title": "" + }, + "MD060": false +} diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 00000000..1507bf0f --- /dev/null +++ b/AGENT.md @@ -0,0 +1,388 @@ +# AGENT.md — RCSPP Library Guide for AI Agents + +This file gives an AI agent the minimum context needed to understand, use, and +extend the `rcspp` library correctly and efficiently. + +--- + +## What the library does + +**RCSPP** solves the *Resource-Constrained Shortest Path Problem*: find the +minimum-cost path from a source node to a sink node in a directed graph, where +each arc consumes one or more resources and the path must stay within resource +limits. + +It is most commonly used as the **pricing subproblem** in column-generation +algorithms for vehicle routing, crew scheduling, and similar combinatorial +optimisation problems. + +--- + +## Repository layout + +```text +cpp/rcspp/ ← C++ header-only library (the "engine") +python/ + bindings/ ← pybind11 C++ source (wraps the C++ engine) + src/rcspp/ ← pure-Python package (the public Python API) +tests/ + cpp/ ← GoogleTest suite (tests/cpp/test_main.cpp + test_*.hpp) + python/ ← pytest suite +examples/ + cpp/ ← VRP C++ benchmark + python/ ← VRP Python example (solve_vrp.py) +extern/pybind11/ ← submodule +``` + +The C++ library is **header-only**. The umbrella include is: + +```cpp +#include "rcspp/rcspp.hpp" +``` + +All C++ symbols live in namespace `rcspp`. + +--- + +## Core data model + +### Graph + +- `ResourceGraph` — main entry point; templated by resource types +- `Graph` — base directed graph (node/arc management, CSR) +- `Node` — has `id`, `source`, `sink`, `in_arcs`, `out_arcs` +- `Arc` — has `id`, `cost`, `origin`, `destination`, `extender`, `rows` +- `Row{int index, double coefficient}` — LP master constraint coefficient + +### Resources + +Each resource slot is defined by four functions: + +| Function | Interface | Responsibility | +|---|---|---| +| `ExtensionFunction` | `extend(current, arc_val, result*)` | How an arc modifies the resource | +| `FeasibilityFunction` | `is_feasible(resource) → bool` | Whether the resource is within bounds | +| `DominanceFunction` | `check_dominance(lhs, rhs) → bool` | Whether lhs makes rhs redundant | +| `CostFunction` | `get_cost(resource) → double` | Resource's contribution to objective | + +Built-in resource types: `RealResource` (double), `IntResource` (int), +`UIntResource`, `SetResource`, `BitsetResource`. + +### Labels + +A `Label` represents a partial path. It stores the accumulated resource state +and a pointer back to the parent label. The solver keeps non-dominated labels +at each node. + +--- + +## Python API — essential patterns + +### Import + +```python +from rcspp.graph import ResourceGraph, AlgorithmParams, BucketAlgorithmParams +from rcspp.resource import ( + AdditionExtensionFunction, SubtractExtensionFunction, + TimeWindowExtensionFunction, TimeWindowFeasibilityFunction, + UnionExtensionFunction, IntersectionExtensionFunction, + NGPathExtensionFunction, + TrivialFeasibilityFunction, MinMaxFeasibilityFunction, + SizeFeasibilityFunction, IntersectionFeasibilityFunction, + TrivialCostFunction, ValueCostFunction, + TrivialDominanceFunction, ValueDominanceFunction, + InclusionDominanceFunction, ContainDominanceFunction, +) +from rcspp.pricing_pool import PricingPool +from rcspp._core.graph import Solution, Column, Row, Algorithm, AlgorithmStatus +``` + +### Build and solve + +```python +rg = ResourceGraph() + +# 1. Register resources (must match order of tuple elements in add_arc) +rg.add_real_resource(ext, feas, cost, dom) # index 0 +rg.add_int_resource(ext, feas, cost, dom) # index 1 + +# 2. Add nodes +rg.add_node(0, source=True) +rg.add_node(1) +rg.add_node(2, sink=True) + +# 3. Add arcs: (resource_tuple, origin, dest, cost, rows) +rg.add_arc((10.0, 3), 0, 1, cost=10.0) +rg.add_arc((5.0, 2), 1, 2, cost=5.0) + +# 4. Solve +result = rg.solve() # returns SolveResult +result = rg.solve(algorithm="simple", # or "greedy", "pushing", "astar", … + upper_bound=-1e-9, # prune cost ≥ this + params=AlgorithmParams(), + preprocess=True, + cost_index=0) +``` + +### SolveResult + +```python +result.solutions # list[Solution], best-first +result.status # AlgorithmStatus enum +result.status_string() # "complete" | "timeout" | "max_solutions" | … +result.num_extended_labels # int + +sol = result.solutions[0] +sol.cost # float +sol.path_node_ids # list[int] +sol.path_arc_ids # list[int] +sol.column # Column(cost, rows=[Row(index, coefficient)]) +``` + +### Column generation + +```python +# Set rows on arcs at construction +rg.add_arc((dist,), i, j, cost=dist, rows=[(constraint_idx, 1.0)]) + +# Update reduced costs from LP duals +rg.update_reduced_costs(duals, cost_index=0) + +# Find negative-RC columns only +result = rg.solve(upper_bound=-1e-9) +``` + +### Arc manipulation + +```python +rg.remove_arcs([arc_id]) # temporarily remove +rg.restore_arcs([arc_id]) # undo removal +rg.remove_arcs_if(lambda arc: …) # predicate-based remove, returns removed ids +rg.restore_arcs_if(lambda arc: …) +rg.clone() # deep copy, independent remove/restore state +``` + +### AlgorithmParams + +```python +p = AlgorithmParams() +p.stop_after_X_solutions = 10 +p.max_iterations = 1000 +p.timeout_s = 30.0 +p.tabu_tenure = 5 +p.seed = 42 +p.limit_to_available_ram = True +p.memory_limit_fraction = 0.8 +``` + +--- + +## C++ API — essential patterns + +### Single resource + +```cpp +using RG = ResourceGraph; +RG graph; +graph.add_resource( + std::make_unique>(), + std::make_unique>(0.0, 100.0), + std::make_unique>(), + std::make_unique>()); + +graph.add_node(0, /*source=*/true); +graph.add_node(1, false, /*sink=*/true); +graph.add_arc({10.0}, 0, 1, /*cost=*/10.0); + +auto result = graph.solve(); +``` + +### Multiple resources + +```cpp +using RG = ResourceGraph; +RG graph; +graph.add_resource(…); // index 0 +graph.add_resource(…); // index 1 + +// arc: {real_val, int_val} +graph.add_arc(std::make_tuple(std::vector{10.0}, std::vector{3}), + origin, dest, cost); +``` + +### Column generation + +```cpp +// Arcs with LP rows +graph.add_arc(…, origin, dest, cost, {Row{constraint_idx, 1.0}}); + +// CG loop +graph.update_reduced_costs(duals, /*cost_index=*/0); +auto result = graph.solve(/*upper_bound=*/-1e-9); +``` + +--- + +## Resource function quick reference + +### Extension + +| Class | Resource | Effect | +|---|---|---| +| `AdditionExtensionFunction` | Numerical | `+=` | +| `SubtractExtensionFunction` | Numerical | `-=` | +| `TimeWindowExtensionFunction(tw)` | Numerical | `max(cur + travel, ready[node])` | +| `UnionExtensionFunction` | Container | `∪=` | +| `IntersectionExtensionFunction` | Container | `∩=` | +| `NGPathExtensionFunction` | Set | add destination node | + +### Feasibility + +| Class | Condition | +|---|---| +| `TrivialFeasibilityFunction` | always true | +| `MinMaxFeasibilityFunction(min, max)` | `min ≤ val ≤ max` | +| `TimeWindowFeasibilityFunction(tw)` | `val ≤ due[node]` | +| `SizeFeasibilityFunction(min, max)` | `min ≤ size ≤ max` | +| `IntersectionFeasibilityFunction` | `∩ ≠ ∅` | + +### Dominance + +| Class | Condition | +|---|---| +| `TrivialDominanceFunction` | never | +| `ValueDominanceFunction` | `lhs ≤ rhs` | +| `InclusionDominanceFunction` | `lhs ⊆ rhs` | +| `ContainDominanceFunction` | `lhs ⊇ rhs` | + +### Cost + +| Class | Returns | +|---|---| +| `TrivialCostFunction` | 0 | +| `ValueCostFunction` | `double(value)` | + +--- + +## Build + +```bash +# C++ only +cmake -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --parallel + +# With Python bindings +cmake -B build -DCMAKE_BUILD_TYPE=Release -DUSE_PYTHON=ON +cmake --build build --parallel + +# CMake options +# USE_PYTHON=ON — build pybind11 extension +# USE_TESTS=ON — build GoogleTest suite (default: ON) +# USE_VRP=ON — build VRP benchmark (needs Gurobi) +# USE_COVERAGE=ON — instrument with gcov (gcc only) +``` + +### FetchContent + +```cmake +FetchContent_Declare(rcspp + GIT_REPOSITORY https://github.com/lab-core/rcspp.git + GIT_TAG main + GIT_SUBMODULES "extern/pybind11") +FetchContent_MakeAvailable(rcspp) +target_link_libraries(my_target PRIVATE rcspp) +``` + +--- + +## Tests + +```bash +# C++ tests +ctest --test-dir build -C Release --output-on-failure + +# Python tests +cd tests/python && pytest -v --tb=short +``` + +Test files: + +- `tests/cpp/test_rcspp.hpp` — labelling, algorithms, graph ops +- `tests/cpp/test_solution_pool.hpp` — SolutionPool API +- `tests/python/test_rcspp.py` — Python solver tests +- `tests/python/test_bindings_coverage.py` — binding coverage + +--- + +## Common pitfalls + +1. **Resource order must match** — the tuple in `add_arc` must have one element + per registered resource, in registration order. + +2. **`update()` / flush** — Python `ResourceGraph` buffers nodes and arcs. + They are flushed automatically before `solve()` and read operations, but not + before `get_nodes_size()` (which counts the buffer). Call `rg.update()` + explicitly if you need the C++ graph up-to-date before a custom operation. + +3. **Preprocessing** — `solve(preprocess=True)` (default) removes arcs that + cannot appear in any optimal path. Arcs are restored after `solve()`, so + the graph is unchanged for subsequent calls. Pass `preprocess=False` to + skip this (e.g. if you call solve in a tight loop and the graph hasn't + changed). + +4. **`update_reduced_costs` does not rebuild the graph** — it only changes arc + costs. You do NOT need to rebuild nodes/arcs between CG iterations. + +5. **`DiversificationSearch` requires `max_iterations`** — without a finite + limit the outer loop never terminates. + +6. **Label containers** — `LabelList` (default) is O(N) per dominance check. + Use `LabelBuckets` + `BucketAlgorithmParams` for problems with many labels + per node (e.g. > 100 non-dominated labels). + +7. **`clone()` is not free** — it deep-copies all nodes, arcs, and the + resource factory. Prefer `remove_arcs` / `restore_arcs` over clone-per-node + in B&B. + +8. **Thread safety** — `ResourceGraph` is NOT thread-safe. `SolutionPool` / + `PricingPool` ARE thread-safe for concurrent `add` + `price` calls. + +--- + +## Extending the library + +### Custom resource type (C++) + +```cpp +struct MyResource { + double value = 0.0; + bool leq(const MyResource& other) const { return value <= other.value; } + bool geq(const MyResource& other) const { return value >= other.value; } + void add(double v) { value += v; } + void reset() { value = 0.0; } + double get_value() const { return value; } +}; + +class MyExtensionFn : public ExtensionFunction { + void extend(const MyResource& cur, const MyResource& arc, + MyResource* res) override { + res->value = cur.value + arc.value * 2.0; // custom rule + } + std::unique_ptr> clone() const override { + return std::make_unique(*this); + } +}; +``` + +### Custom algorithm (C++) + +Inherit from `Algorithm` and override +`main_loop()`: + +```cpp +template> +class MyAlgorithm : public Algorithm { + protected: + void main_loop() override { /* custom label expansion */ } +}; +``` diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 00000000..43766a9b --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,5 @@ +source "https://rubygems.org" + +gem "jekyll", "~> 4.3" +gem "jekyll-remote-theme" +gem "webrick" diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 00000000..cd2dafa0 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,29 @@ +title: RCSPP +description: Resource-Constrained Shortest Path Problem — C++ library with Python bindings +remote_theme: just-the-docs/just-the-docs@v0.10.1 + +url: https://lab-core.github.io/rcspp + +logo: # leave blank (no logo image yet) + +search_enabled: true +search: + heading_level: 3 + previews: 2 + +aux_links: + GitHub: https://github.com/lab-core/rcspp + +aux_links_new_tab: true + +footer_content: "© Laboratory for Combinatorial Optimization in Real-time Environment (LCORE). Released under the MIT License." + +nav_order: + - index.md + - installation.md + - cpp + - python + - advanced + +plugins: + - jekyll-remote-theme diff --git a/docs/advanced/algorithms.md b/docs/advanced/algorithms.md new file mode 100644 index 00000000..a80db07f --- /dev/null +++ b/docs/advanced/algorithms.md @@ -0,0 +1,99 @@ +--- +title: Algorithms +parent: Advanced +nav_order: 1 +--- + +# Algorithms + +## Choosing an algorithm + +| Algorithm | Optimal | Speed | When to use | +|---|---|---|---| +| `Simple` | ✓ | medium | Default; correct answer needed | +| `Pushing` | ✓ | medium–fast | Dense graphs; pushing dominance forward | +| `Pulling` | ✓ | medium–fast | Bi-directional; long paths | +| `AStar` | ✓ | fast–slow | Good heuristic available | +| `Greedy` | ✗ | fast | Quick feasible solution; large graphs | +| `Tabu` | ✗ | medium | Metaheuristic diversity | +| `Diversification` | ✗ | configurable | Generate a diverse pool of solutions | + +## `Simple` (default) + +Classic forward label-setting. Processes nodes in topological order and +propagates labels; dominated labels are discarded immediately. + +```python +result = rg.solve() # algorithm="simple" by default +result = rg.solve(algorithm="simple") +``` + +## `Greedy` + +Extends labels greedily (best-cost-first) with limited backtracking. +Much faster than the exact algorithms; no optimality guarantee. + +```python +params = AlgorithmParams() +params.num_labels_to_extend_by_node = 5 # extend at most 5 labels per node + +result = rg.solve(algorithm="greedy", params=params) +``` + +## `Tabu` + +Tabu-arc metaheuristic. At each iteration it removes the arcs used in the +current solution from the graph and re-solves, diversifying the search. +Arc removal is temporary (governed by `tabu_tenure`). + +```python +params = AlgorithmParams() +params.tabu_tenure = 5 +params.tabu_random_noise = True # random ±1 tenure noise +params.max_iterations = 100 +params.forbidden_tabu = {source_id, sink_id} # never remove arcs from these + +result = rg.solve(algorithm="tabu", params=params) +``` + +## `Diversification` + +Wraps another algorithm (default: `Greedy`) and repeatedly solves the +subproblem after removing solution arcs, collecting a pool of diverse +solutions. + +```python +params = AlgorithmParams() +params.stop_after_X_solutions = 20 # collect 20 distinct solutions +params.max_iterations = 100 +params.seed = 42 + +result = rg.solve(algorithm="greedy", params=params) +# In C++, use DiversificationSearch explicitly to wrap any inner algorithm +``` + +## Bucket label containers + +For graphs with many labels per node, `LabelBuckets` speeds up dominance +checking by partitioning labels on one resource dimension. + +```python +from rcspp.graph import BucketAlgorithmParams + +bp = BucketAlgorithmParams() +bp.range_buckets = 200 # partition the time dimension into 200 buckets +bp.bucket_resource_index = 0 # resource 0 is time +bp.sort_resource_index = 1 # sort within bucket by resource 1 + +result = rg.solve(algorithm="simple", params=bp) +``` + +In C++: + +```cpp +BucketAlgorithmParams> bp; +bp.range_buckets = 200; +bp.bucket_resource_index = 0; +bp.sort_resource_index = 1; +auto result = graph.solve(ub, bp); +``` diff --git a/docs/advanced/column-generation.md b/docs/advanced/column-generation.md new file mode 100644 index 00000000..afe9eb59 --- /dev/null +++ b/docs/advanced/column-generation.md @@ -0,0 +1,181 @@ +--- +title: Column Generation +parent: Advanced +nav_order: 2 +--- + +# Column Generation + +RCSPP arises naturally as the pricing subproblem in column-generation (CG) +algorithms for vehicle routing and crew scheduling. This page describes the +full integration pattern. + +## Concept + +In a CG loop the LP master provides dual values `π` (one per constraint). The +pricing subproblem finds a path (column) with *negative reduced cost*: + +```text +reduced_cost = arc_cost_sum - Σ π[i] * coefficient[i] +``` + +If no such column exists, the LP is optimal. + +Each arc carries a list of `Row` objects `(index, coefficient)` describing its +contribution to each LP constraint. The solver recomputes arc costs from duals +via `update_reduced_costs(duals)`, then calls `solve(upper_bound=-1e-9)` to +find only negative-RC columns. + +--- + +## Minimal Python CG loop + +```python +import math +from rcspp.graph import ResourceGraph +from rcspp.resource import ( + AdditionExtensionFunction, MinMaxFeasibilityFunction, + ValueCostFunction, ValueDominanceFunction, +) + +# Build graph +rg = ResourceGraph() +rg.add_real_resource( + AdditionExtensionFunction(), + MinMaxFeasibilityFunction(0.0, math.inf), + ValueCostFunction(), + ValueDominanceFunction(), +) + +n_customers = 5 +for i in range(n_customers + 2): + rg.add_node(i, source=(i == 0), sink=(i == n_customers + 1)) + +for i in range(n_customers + 1): + for j in range(1, n_customers + 2): + if i != j: + dist = distance_matrix[i][j] + # Row: this arc "visits" customer j → constraint index j-1 + rg.add_arc( + (dist,), i, j, cost=dist, + rows=[(j - 1, 1.0)] if 1 <= j <= n_customers else [], + ) + +# CG loop +duals = [0.0] * n_customers + +while True: + rg.update_reduced_costs(duals, cost_index=0) + result = rg.solve(upper_bound=-1e-9) + + if not result.solutions: + break # LP optimal + + for sol in result.solutions: + master.add_column(sol.column) + + duals = master.solve_and_get_duals() +``` + +--- + +## `PricingPool` — column pool with activity tracking + +For large CG applications, reusing previously found columns across iterations +is critical. `PricingPool` manages the column pool and exposes fast +numpy-based pricing. + +```python +from rcspp.pricing_pool import PricingPool +import numpy as np + +pool = PricingPool(n_constraints=n_customers, max_cols=100_000) + +# CG loop +duals = np.zeros(n_customers) + +while True: + rg.update_reduced_costs(duals.tolist()) + result = rg.solve(upper_bound=-1e-9) + + if result.solutions: + new_ids = pool.add_columns(result.solutions) + for sol in result.solutions: + master.add_column(sol.column) + + # Price ALL stored columns (fast numpy path) + ids, rcs = pool.price(duals, threshold=-1e-9) + + if len(ids) == 0 and not result.solutions: + break # no new and no stored negative-RC column + + # Mark which columns are in the LP basis (for activity tracking) + pool.update_activity(master.get_basis_column_ids()) + duals = np.array(master.solve_and_get_duals()) +``` + +### B&B node filtering + +At each branch-and-bound node, certain arcs are forbidden. Create a +`FilteredPricingPool` view instead of rebuilding the pool: + +```python +node_pool = pool.new_filter(forbidden_arc_ids=[forbidden_arc]) +ids, rcs = node_pool.price(duals) +``` + +### Cleanup + +```python +# Remove columns that have not been used recently +pool.remove_stale(max_age=50, min_usage_rate=0.01) + +# Or custom predicate +pool.global_remove_if(lambda col_id, sol, act: act.age > 100) +``` + +--- + +## Cross-process parallel pricing + +For parallel B&B or multi-commodity CG, share the column pool across +processes using shared memory: + +```python +# Master process +pool = PricingPool(n_constraints=200, max_cols=50_000) +handle = pool.handle() # serialisable dict — pass to workers + +# Worker process (no pybind11 C++ needed — numpy only) +from rcspp.pricing_pool import PricingPool +shared = PricingPool.attach(handle) # SharedPricingPool +indices, rcs = shared.price(np.array(duals)) +``` + +The shared pool uses a memory-mapped numpy array; `price()` is lock-free and +safe to call from multiple processes simultaneously. + +--- + +## Arc rows: multiple commodity constraints + +```python +# Arc visits customer 3 (constraint 3) AND uses one vehicle (constraint 0) +rg.add_arc( + (dist,), i, j, cost=dist, + rows=[(0, 1.0), (3, 1.0)], +) +``` + +Rows can also be added after construction: + +```python +rg.add_rows_to_arc(arc_id, [(constraint_idx, coeff)]) + +# Bulk add (numpy-friendly) +rg.add_rows(np.array([ + [arc_id_0, constraint_0, coeff_0], + [arc_id_1, constraint_1, coeff_1], + … +])) +``` diff --git a/docs/advanced/index.md b/docs/advanced/index.md new file mode 100644 index 00000000..26fb6017 --- /dev/null +++ b/docs/advanced/index.md @@ -0,0 +1,7 @@ +--- +title: Advanced +nav_order: 5 +has_children: true +--- + +# Advanced Topics diff --git a/docs/cpp/api.md b/docs/cpp/api.md new file mode 100644 index 00000000..207fb0cf --- /dev/null +++ b/docs/cpp/api.md @@ -0,0 +1,311 @@ +--- +title: C++ API Reference +parent: C++ Library +nav_order: 3 +--- + +# C++ API Reference + +All classes are in the `rcspp` namespace. Include `rcspp/rcspp.hpp`. + +--- + +## `ResourceGraph` + +The main entry point. Template parameters are the resource types in the +order they are registered. + +```cpp +template +class ResourceGraph : public Graph> { … }; +``` + +### Construction + +```cpp +ResourceGraph graph; + +// Reserve capacity upfront (optional — avoids rehashing) +graph.reserve(n_nodes, n_arcs); +``` + +### Adding resources + +Resources must be added in the same order as the template parameters. + +```cpp +graph.add_resource( + std::make_unique>(), + std::make_unique>(0.0, 100.0), + std::make_unique>(), + std::make_unique>()); + +graph.add_resource( + std::make_unique>(), + std::make_unique>(0, 10), + std::make_unique>(), + std::make_unique>()); +``` + +### Adding nodes and arcs + +```cpp +// Nodes +Node& n = graph.add_node(node_id, /*source=*/false, /*sink=*/false); + +// Arcs — single resource +graph.add_arc({10.0}, origin_id, dest_id, arc_cost); + +// Arcs — two resources +graph.add_arc( + std::make_tuple(std::vector{10.0}, std::vector{3}), + origin_id, dest_id, arc_cost, + {Row{constraint_index, coefficient}}); // optional LP rows +``` + +### Solving + +```cpp +// Default: SimpleDominanceAlgorithm, LabelList +SolveResult result = graph.solve(); +SolveResult result = graph.solve(upper_bound, params, /*preprocess=*/true, /*cost_index=*/0); + +// Explicit algorithm and label container +SolveResult result = graph.solve(); +SolveResult result = graph.solve( + upper_bound, bucket_params); +``` + +### Graph modification + +```cpp +bool removed = graph.remove_arc(arc_id); +bool restored = graph.restore_arc(arc_id); + +// Force an arc — removes all other arcs between same origin–destination pair +std::vector removed_ids = graph.force_arc(arc_id); + +// Accessors +Node* node = graph.get_node(node_id); +Arc* arc = graph.get_arc(arc_id); + +// Clone (deep copy with independent arc-removal state) +auto copy = graph.clone(/*include_rows=*/true, /*clone_removed_arcs=*/false); +``` + +### Column generation + +```cpp +// Recompute arc reduced costs from LP dual values +// reduced_cost = arc.cost - Σ (row.coefficient * duals[row.index]) +graph.update_reduced_costs(duals, /*cost_index=*/0); +``` + +--- + +## `Graph` — base class + +Underlying directed graph structure. Normally you work through +`ResourceGraph`, but the base is accessible. + +```cpp +size_t number_of_nodes() const; +size_t number_of_arcs() const; +const std::vector*>& get_sorted_nodes() const; // topological order +const std::vector& get_source_node_ids() const; +const std::vector& get_sink_node_ids() const; +void sort_nodes(); // topological sort +void build_csr(); // build compressed-row adjacency (needed after arc changes for some algos) +``` + +--- + +## `Node` and `Arc` + +### Node + +```cpp +struct Node { + size_t id; + bool source, sink; + size_t pos; // position in sorted order + Resource resource; + std::vector*> in_arcs, out_arcs; +}; +``` + +### Arc + +```cpp +struct Arc { + size_t id; + double cost; + Node* origin; + Node* destination; + std::unique_ptr> extender; + std::vector rows; // LP column coefficients +}; +``` + +### Row + +```cpp +struct Row { + int index; // constraint index in the LP master + double coefficient; +}; +``` + +--- + +## `AlgorithmParams` + +Controls solver behaviour. Pass as the second argument to `solve()`. + +```cpp +AlgorithmParams> params; + +// Termination +params.stop_after_X_solutions = 1; // stop after finding N solutions +params.max_iterations = 100; // max label extension iterations +params.timeout_s = 60.0; // wall-clock timeout +params.should_stop = []{ return external_flag; }; // custom callback + +// Optimality +params.return_dominated_solutions = false; // include dominated solutions in output +params.num_max_phases = 1; // number of DP phases (bi-directional: 2) + +// Column generation +params.use_pool = true; +params.release_after_solve = true; // shrink label pool after solve + +// Memory limits +params.max_memory_gb = 8.0; +params.limit_to_available_ram = false; +params.memory_limit_fraction = 0.9; +params.memory_pressure_fraction = 0.8; +params.memory_pressure_max_labels_per_node = 200; +params.memory_check_interval = 50000; + +// Greedy/tabu parameters +params.num_labels_to_extend_by_node = MAX_INT; +params.tabu_tenure = 5; +params.tabu_random_noise = true; +params.forbidden_tabu = {source_id, sink_id}; +params.seed = 42; +``` + +### `BucketAlgorithmParams>` + +Additional parameters when using `LabelBuckets`: + +```cpp +BucketAlgorithmParams> bp; +bp.range_buckets = 100; // number of buckets +bp.bucket_resource_index = 0; // resource to partition on +bp.sort_resource_index = 1; // resource to sort within bucket +``` + +--- + +## `SolveResult` + +```cpp +struct SolveResult { + std::vector solutions; // sorted best-first + AlgorithmStatus status; + size_t num_extended_labels; + std::string status_string() const; +}; + +enum class AlgorithmStatus { + COMPLETE, // all non-dominated paths found + TIMEOUT, + MAX_SOLUTIONS, + MAX_PHASES, + INTERRUPTED, // SIGINT + MEMORY_LIMIT, +}; +``` + +--- + +## `Solution` + +```cpp +struct Solution { + double cost; + std::vector path_node_ids; + std::vector path_arc_ids; + Column column; // for LP master +}; + +struct Column { + double cost; + std::vector rows; +}; +``` + +--- + +## Algorithms + +All algorithms inherit from `Algorithm`. + +| Class | Optimal | Notes | +|---|---|---| +| `SimpleDominanceAlgorithm` | Yes | Standard label-setting; default | +| `PushingDominanceAlgorithm` | Yes | Pushes dominated labels forward | +| `PullingDominanceAlgorithm` | Yes | Bi-directional with pull phase | +| `AStarDominanceAlgorithm` | Yes | A★ heuristic for priority | +| `GreedyAlgorithm` | No | Fast; good first solution | +| `TabuSearch` | No | Tabu-arc avoidance | +| `ImprovingTabuSearch` | No | Tabu + improving-move filter | +| `DiversificationSearch` | No | Wraps another algorithm; collects diverse solutions | + +### Choosing an algorithm + +```cpp +// Via template parameter on ResourceGraph::solve +graph.solve(ub, params); + +// Via explicit construction +auto algo = std::make_unique>( + &resource_factory, params, std::move(inner_algo)); +auto result = algo->solve(&graph, upper_bound); +``` + +--- + +## `SolutionPool` (column generation) + +Thread-safe column store with activity tracking. Used in conjunction with +`FilteredSolutionPool` for per-subproblem views. + +```cpp +SolutionPool pool; + +// Create a view +auto& fp = pool.new_filter(); // FilteredSolutionPool& + +// Add solutions +size_t col_id = fp.add(solution); +std::vector ids = fp.add({sol1, sol2, sol3}); + +// Price columns (returns those with reduced_cost < threshold) +auto priced = fp.price(duals, threshold); + +// Activity +auto& act = fp.get_activity(col_id); +// act.age, act.use_count, act.priced_count, act.usage_rate() + +// Remove stale columns +fp.remove_stale(max_age); +fp.global_remove_if([](size_t id, const Solution& s, const Activity& a) { + return a.usage_rate() < 0.01; +}); + +// Arc-based filters +fp.remove_if_arc_present(forbidden_arc_id); +fp.global_remove_if_arc_present(arc_id); +``` diff --git a/docs/cpp/concepts.md b/docs/cpp/concepts.md new file mode 100644 index 00000000..353bbf88 --- /dev/null +++ b/docs/cpp/concepts.md @@ -0,0 +1,202 @@ +--- +title: Concepts +parent: C++ Library +nav_order: 1 +--- + +# Core Concepts + +## The Label-Setting Algorithm + +RCSPP is solved by a *label-setting* algorithm. A **label** represents a +partial path from a source node to some intermediate node, together with the +accumulated resource state along that path. + +At each node the algorithm maintains a set of non-dominated labels. A label +`L₁` *dominates* `L₂` when `L₁` can reach the sink at least as cheaply as +`L₂` while consuming no more of any resource — so `L₂` can be discarded. + +### Label lifecycle + +```text +source node + │ + ▼ extend label along arc (extension function) +intermediate node + │ + ▼ feasibility check (feasibility function) + │ dominance check against existing labels (dominance function) + │ + ├─ dominated? discard + └─ non-dominated? keep, add to node's label set + │ + ▼ sink node + extract path, compute cost (cost function) +``` + +--- + +## Resource composition + +`ResourceGraph` is parametrised by one or more resource types. +Internally the variadic types are wrapped in a +`ResourceTypeComposition` whose extension, feasibility, dominance, +and cost functions are the **composition** of the individual functions. + +Two labels are compared component-wise: `L₁ ≤ L₂` iff every resource of `L₁` +dominates the corresponding resource of `L₂`. + +--- + +## Built-in resource types + +| C++ type | Python name | Underlying value | Typical use | +|---|---|---|---| +| `RealResource` | `"real"` | `double` | Distance, time, cost | +| `IntResource` | `"int"` | `int` | Demand, hop count | +| `UIntResource` | `"uint"` | `unsigned int` | Unsigned quantities | +| `SetResource` | `"int_set"` | `std::set` | Visited customers | +| `SetResource` | `"real_set"` | `std::set` | | +| `BitsetResource` | `"bitset"` | `boost::dynamic_bitset` | Compact visited-node tracking | + +--- + +## Resource functions + +Each resource slot has four functions that you supply at construction time. + +### Extension function + +Defines how an arc modifies the resource as a label is extended along it. + +```cpp +template +class ExtensionFunction { + virtual void extend( + const ResourceType& current, // label's resource at origin + const ResourceType& arc_value, // arc's resource consumption + ResourceType* result) = 0; // written in-place +}; +``` + +Built-in implementations: + +| Class | Effect | +|---|---| +| `AdditionExtensionFunction` | `result = current + arc_value` | +| `SubtractExtensionFunction` | `result = current - arc_value` | +| `TimeWindowExtensionFunction` | `result = max(current + travel, ready_time)` | +| `UnionExtensionFunction` | `result = current ∪ arc_value` | +| `IntersectionExtensionFunction` | `result = current ∩ arc_value` | +| `NGPathExtensionFunction` | Adds visited node (NG-path extension) | + +### Feasibility function + +Returns `true` when a label's resource value is still within bounds. + +```cpp +template +class FeasibilityFunction { + virtual bool is_feasible(const ResourceType& resource) = 0; +}; +``` + +Built-in implementations: + +| Class | Condition | +|---|---| +| `TrivialFeasibilityFunction` | Always `true` | +| `MinMaxFeasibilityFunction(min, max)` | `min ≤ value ≤ max` | +| `TimeWindowFeasibilityFunction(tw)` | `value ≤ due_time[node]` | +| `SizeFeasibilityFunction(min, max)` | `min ≤ container.size() ≤ max` | +| `IntersectionFeasibilityFunction` | `current ∩ arc_value ≠ ∅` | +| `ReachableFeasibilityFunction` | Can still reach a sink | + +### Dominance function + +Returns `true` when `lhs` dominates `rhs` (i.e. `rhs` can be pruned). + +```cpp +template +class DominanceFunction { + virtual bool check_dominance( + const ResourceType& lhs, const ResourceType& rhs) = 0; +}; +``` + +Built-in implementations: + +| Class | Condition | +|---|---| +| `TrivialDominanceFunction` | Never dominates (keep all labels) | +| `ValueDominanceFunction` | `lhs.value ≤ rhs.value` | +| `InclusionDominanceFunction` | `lhs ⊆ rhs` | +| `ContainDominanceFunction` | `lhs ⊇ rhs` | + +### Cost function + +Returns the contribution of this resource slot to the label's total cost. + +```cpp +template +class CostFunction { + virtual double get_cost(const ResourceType& resource) const = 0; +}; +``` + +Built-in implementations: + +| Class | Returns | +|---|---| +| `TrivialCostFunction` | `0.0` | +| `ValueCostFunction` | `static_cast(resource.value)` | + +--- + +## Arc consumption + +Each arc stores a `unique_ptr>`. The extender bundles +the arc's resource consumption value together with the pre-bound extension +function, so extending a label along an arc is a single virtual call. + +Arc resource consumption is specified at construction time: + +```cpp +// Single resource +graph.add_arc({10.0}, origin, dest, arc_cost); + +// Two resources +graph.add_arc(std::make_tuple(std::vector{10.0}, std::vector{3}), + origin, dest, arc_cost); +``` + +--- + +## Label containers + +The algorithm stores non-dominated labels at each node in a *label container*. +Two built-in containers are provided: + +| Type | Complexity | Best for | +|---|---|---| +| `LabelList` (default) | O(N) dominance check | Few labels per node | +| `LabelBuckets` | O(log B) dominance check | Many labels; partitioned by one resource | + +`LabelBuckets` requires a `BucketAlgorithmParams` specifying the bucket +resource index and sort resource index. + +--- + +## Preprocessing + +Before the main label loop, the solver (when `preprocess=true`) runs: + +1. **Feasibility preprocessor** — removes arcs whose resource consumption + makes them immediately infeasible. +2. **Shortest-path preprocessor** — uses a Bellman-Ford variant to remove + arcs that cannot appear in any optimal path. +3. **Connectivity check** — ensures source and sink are connected; marks + reachable subsets. + +Removed arcs are restored automatically after `solve()` returns so the graph +can be reused in a column-generation loop. diff --git a/docs/cpp/index.md b/docs/cpp/index.md new file mode 100644 index 00000000..3f3cb030 --- /dev/null +++ b/docs/cpp/index.md @@ -0,0 +1,49 @@ +--- +title: C++ Library +nav_order: 3 +has_children: true +--- + +# C++ Library + +The C++ core is a **header-only** template library under `cpp/rcspp/`. Include +the umbrella header and you get everything: + +```cpp +#include "rcspp/rcspp.hpp" +``` + +All classes live in the `rcspp` namespace. + +## Header organisation + +```text +cpp/rcspp/ +├── rcspp.hpp # umbrella header +├── resource/ +│ ├── resource_graph.hpp # ResourceGraph<...> — main entry point +│ ├── base/ # ExtensionFunction, FeasibilityFunction, … +│ └── concrete/ # NumericalResource, ContainerResource, built-ins +├── graph/ +│ ├── graph.hpp # Graph base +│ ├── node.hpp, arc.hpp +│ └── row.hpp +├── algorithm/ +│ ├── algorithm.hpp # Algorithm base, AlgorithmParams, SolveResult +│ ├── simple_dominance_algorithm.hpp +│ ├── pushing_dominance_algorithm.hpp +│ ├── pulling_dominance_algorithm.hpp +│ ├── greedy.hpp +│ ├── tabu_search.hpp / improving_tabu_search.hpp +│ ├── astar_dominance_algorithm.hpp +│ ├── diversification_search.hpp +│ ├── label_buckets.hpp # LabelList, LabelBuckets +│ ├── solution.hpp +│ └── solution_pool.hpp +├── label/ +│ ├── label.hpp, label_pool.hpp, label_factory.hpp +└── preprocessor/ + ├── shortest_path_preprocessor.hpp + ├── feasibility_preprocessor.hpp + └── connectivity_matrix.hpp +``` diff --git a/docs/cpp/quickstart.md b/docs/cpp/quickstart.md new file mode 100644 index 00000000..21f7e74b --- /dev/null +++ b/docs/cpp/quickstart.md @@ -0,0 +1,152 @@ +--- +title: C++ Quick Start +parent: C++ Library +nav_order: 2 +--- + +# C++ Quick Start + +## Minimal example — single resource + +```cpp +#include "rcspp/rcspp.hpp" +using namespace rcspp; + +// Graph with one real resource (distance, max = 50) +using RG = ResourceGraph; + +RG graph; +graph.add_resource( + std::make_unique>(), + std::make_unique>(0.0, 50.0), + std::make_unique>(), + std::make_unique>()); + +graph.add_node(0, /*source=*/true); +graph.add_node(1); +graph.add_node(2); +graph.add_node(3, false, /*sink=*/true); + +graph.add_arc({10.0}, 0, 1, /*cost=*/10.0); +graph.add_arc({15.0}, 1, 3, /*cost=*/15.0); +graph.add_arc({20.0}, 0, 2, /*cost=*/20.0); +graph.add_arc({10.0}, 2, 3, /*cost=*/10.0); +// path 0→2→3 has lower cost (30) but identical resource usage to 0→1→3 (25) + +SolveResult result = graph.solve(); +std::cout << "Cost: " << result.solutions[0].cost << "\n"; // 25.0 +``` + +--- + +## Two resources — time window + capacity + +```cpp +#include "rcspp/rcspp.hpp" +using namespace rcspp; + +using RG = ResourceGraph; + +// time windows per node: {node_id: {ready, due}} +std::unordered_map> tw = { + {0, {0.0, 0.0}}, + {1, {5.0, 20.0}}, + {2, {0.0, 100.0}}, +}; + +RG graph; + +// Resource 0: time (drives objective via ValueCostFunction) +graph.add_resource( + std::make_unique>(tw), + std::make_unique>(tw), + std::make_unique>(), + std::make_unique>()); + +// Resource 1: demand (capacity ≤ 10, no cost) +graph.add_resource( + std::make_unique>(), + std::make_unique>(0, 10), + std::make_unique>(), + std::make_unique>()); + +graph.add_node(0, true); +graph.add_node(1); +graph.add_node(2, false, true); + +// arc: {travel_time, demand} +graph.add_arc(std::make_tuple(std::vector{5.0}, std::vector{3}), 0, 1, 5.0); +graph.add_arc(std::make_tuple(std::vector{8.0}, std::vector{4}), 1, 2, 8.0); + +auto result = graph.solve(); +``` + +--- + +## Column generation loop + +```cpp +#include "rcspp/rcspp.hpp" +using namespace rcspp; + +using Comp = ResourceTypeComposition; +using RG = ResourceGraph; + +RG graph; +// … build graph with rows on arcs … + +// arc with LP row: arc contributes 1 unit to constraint 5 +graph.add_arc({10.0, 2}, 0, 1, 10.0, {Row{5, 1.0}}); + +// Solve loop +std::vector duals(n_constraints, 0.0); + +while (true) { + graph.update_reduced_costs(duals); + SolveResult result = graph.solve(/*upper_bound=*/-1e-9); + + if (result.solutions.empty()) break; // no negative-RC column + + for (auto& sol : result.solutions) { + master_problem.add_column(sol.column); // hand column to LP solver + } + + duals = master_problem.get_duals(); // solve LP, get new duals +} +``` + +--- + +## Choosing an algorithm + +```cpp +// Default (optimal): SimpleDominanceAlgorithm +auto result = graph.solve(); + +// Greedy (fast, non-optimal) +auto result = graph.solve(ub, params); + +// Diversification search — collect N diverse solutions +AlgorithmParams> params; +params.stop_after_X_solutions = 10; +params.max_iterations = 50; +auto result = graph.solve(ub, params); +``` + +--- + +## Memory-limited solve + +```cpp +AlgorithmParams> params; +params.limit_to_available_ram = true; // auto-limit to system free RAM +params.memory_limit_fraction = 0.8; // use at most 80% of available RAM +params.memory_pressure_fraction = 0.6; // start pruning at 60% +params.memory_pressure_max_labels_per_node = 50; // prune to 50 labels/node under pressure +params.memory_check_interval = 10000; // check every 10k label extensions + +auto result = graph.solve(ub, params); +if (result.status == AlgorithmStatus::MEMORY_LIMIT) { + // solver hit the RSS limit; solutions may be incomplete +} +``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..32a4c5df --- /dev/null +++ b/docs/index.md @@ -0,0 +1,129 @@ +--- +title: Home +layout: home +nav_order: 1 +--- + +# RCSPP + +**RCSPP** is a modern C++23 library for solving +[Resource-Constrained Shortest Path Problems (RCSPP)](https://en.wikipedia.org/wiki/Constrained_shortest_path_first) +with optional Python bindings via [pybind11](https://github.com/pybind/pybind11). + +--- + +## What is the RCSPP? + +Given a directed graph where each arc consumes resources (time, capacity, cost …), find the +minimum-cost path from source to sink that stays within all resource limits. + +The library models each resource independently through four pluggable functions: + +| Function | Role | +|---|---| +| **Extension** | How traversing an arc changes the resource value | +| **Feasibility** | Whether the current resource value is still within bounds | +| **Dominance** | Whether one partial path makes another redundant | +| **Cost** | The contribution of this resource to the objective | + +The solver maintains a set of *labels* (partial paths with accumulated resource state) and +prunes dominated ones, yielding an exact (or heuristic) set of optimal solutions. + +--- + +## Quick Example + +### Python + +```python +from rcspp.graph import ResourceGraph +from rcspp.resource import ( + AdditionExtensionFunction, + MinMaxFeasibilityFunction, + TrivialCostFunction, + ValueCostFunction, + ValueDominanceFunction, +) + +rg = ResourceGraph() + +# Resource 0: cumulative distance — drives the objective (ValueCostFunction) +rg.add_real_resource( + AdditionExtensionFunction(), + MinMaxFeasibilityFunction(0.0, 50.0), # max distance = 50 + ValueCostFunction(), + ValueDominanceFunction(), +) + +# Graph: 0 → 1 → 3 (cost 25, distance 25) +# 0 → 2 → 3 (cost 25, distance 30) +rg.add_node(0, source=True) +rg.add_node(1) +rg.add_node(2) +rg.add_node(3, sink=True) +rg.add_arc((10.0,), 0, 1, cost=10.0) +rg.add_arc((15.0,), 1, 3, cost=15.0) +rg.add_arc((20.0,), 0, 2, cost=20.0) +rg.add_arc((10.0,), 2, 3, cost=10.0) # cheaper but farther + +result = rg.solve() +print(result.solutions[0].cost) # 25.0 +print(result.solutions[0].path_node_ids) # [0, 1, 3] +``` + +### C++ + +```cpp +#include "rcspp/rcspp.hpp" + +using namespace rcspp; +using RG = ResourceGraph; + +RG graph; +graph.add_resource( + std::make_unique>(), + std::make_unique>(0.0, 50.0), + std::make_unique>(), + std::make_unique>()); + +graph.add_node(0, /*source=*/true); +graph.add_node(1); +graph.add_node(2); +graph.add_node(3, false, /*sink=*/true); +graph.add_arc({10.0}, 0, 1, 10.0); +graph.add_arc({15.0}, 1, 3, 15.0); +graph.add_arc({20.0}, 0, 2, 20.0); +graph.add_arc({10.0}, 2, 3, 10.0); + +auto result = graph.solve(); +std::cout << result.solutions[0].cost << "\n"; // 25.0 +``` + +--- + +## Key Features + +- **Multiple resource types** — `real`, `int`, `uint`, `real_set`, `int_set`, `bitset` and more +- **Multiple algorithms** — Simple Dominance, Pushing/Pulling Dominance, Greedy, Tabu Search, A★, Diversification +- **Column generation support** — `PricingPool` with deduplication, activity tracking, and cross-process shared memory +- **Preprocessing** — automatic arc removal via Bellman-Ford + connectivity analysis +- **Memory management** — configurable RSS limits, label pool recycling +- **NetworkX interop** — construct graphs directly from `nx.DiGraph` +- **Header-only C++ core** — drop into any CMake project via `FetchContent` + +--- + +## Repository Layout + +```text +rcspp/ +├── cpp/rcspp/ # C++ headers (header-only library) +├── python/ +│ ├── bindings/ # pybind11 C++ bindings +│ └── src/rcspp/ # Pure-Python package +├── examples/ # C++ benchmark and Python VRP examples +├── tests/ +│ ├── cpp/ # GoogleTest suite +│ └── python/ # pytest suite +└── extern/pybind11/ # submodule +``` diff --git a/docs/python/api.md b/docs/python/api.md new file mode 100644 index 00000000..a84944ba --- /dev/null +++ b/docs/python/api.md @@ -0,0 +1,328 @@ +--- +title: Python API Reference +parent: Python Package +nav_order: 3 +--- + +# Python API Reference + +--- + +## `ResourceGraph` + +```python +from rcspp.graph import ResourceGraph +rg = ResourceGraph(nx_graph=None) +``` + +### Resource registration + +Must be called before adding nodes/arcs. The order of calls defines the +resource index used in `add_arc` tuples and `cost_index`. + +```python +# Numerical resources +rg.add_real_resource(ext, feas, cost, dom) # RealResource (float) +rg.add_int_resource(ext, feas, cost, dom) # IntResource (int) +rg.add_uint_resource(ext, feas, cost, dom) # UIntResource (unsigned int) + +# Container resources +rg.add_real_set_resource(ext, feas, cost, dom) # set[float] +rg.add_int_set_resource(ext, feas, cost, dom) # set[int] +rg.add_bitset_resource(ext, feas, cost, dom) # bitset +``` + +Each argument is an instance of the corresponding function class from +`rcspp.resource` (see [Resource Functions](#resource-functions)). + +### Building the graph + +```python +rg.reserve(n_nodes, n_arcs) # optional: pre-allocate capacity + +rg.add_node(node_id, source=False, sink=False) + +arc_id = rg.add_arc( + resource_consumption, # tuple of values, one per registered resource + origin_id, + destination_id, + cost=0.0, + rows=None, # list of (constraint_index, coefficient) or Row objects +) + +rg.update() # flush buffers to C++ (called automatically before solve/get_*) +``` + +`resource_consumption` is a flat tuple when all resources are scalar: +`(real_val, int_val)`. For container resources, each element is itself a +collection: `({1, 2, 3}, 5.0)`. + +### Reading the graph + +```python +node = rg.get_node(node_id) # Node object or None +arc = rg.get_arc(arc_id) # Arc object or None +arcs = rg.get_arcs(origin, dest) # list[Arc] + +rg.get_nodes_size() # int (includes buffered) +rg.get_arcs_size() # int (includes buffered) +``` + +### Arc modification + +```python +# Remove / restore individual arcs +removed = rg.remove_arcs([arc_id, …]) # list[int] — actually removed +restored = rg.restore_arcs([arc_id, …]) # list[int] — actually restored + +# In-place arc modification +rg.update_arc(arc, new_consumption) + +# Append LP rows +rg.add_rows_to_arc(arc_id, [(constraint_idx, coeff), …]) +rg.add_rows([ + (arc_id, constraint_idx, coeff), + … +]) + +# arc_ids iterator +ids = rg.arc_ids() # list[int] + +# Remove/restore by predicate (returns affected arc ids) +removed = rg.remove_arcs_if(lambda arc: arc.id % 2 == 0) +restored = rg.restore_arcs_if(lambda arc: arc.id % 2 == 0) +``` + +### Solving + +```python +result = rg.solve( + algorithm="simple", # "simple" | "pushing" | "pulling" | "greedy" | "astar" + # or Algorithm enum from rcspp._core.graph + upper_bound=math.inf, + params=None, # AlgorithmParams or BucketAlgorithmParams + preprocess=True, + cost_index=0, # which registered real/int resource is the objective +) +``` + +Returns a `SolveResult` (see below). + +### Cloning + +```python +clone = rg.clone(include_rows=True, clone_removed_arcs=False) +topo = rg.clone_topology() # same as clone(include_rows=False) +``` + +### Column generation + +```python +rg.update_reduced_costs(duals, cost_index=0) +# Updates each arc's cost to: arc.cost - Σ(row.coeff * duals[row.index]) +``` + +### NetworkX + +```python +rg = ResourceGraph(nx_graph) # construct from nx.DiGraph + # nodes: attrs 'source', 'sink' + # edges: attrs 'cost', 'consumption' +rg.from_networkx(nx_graph) # rebuild from a new nx.DiGraph +``` + +--- + +## `AlgorithmParams` + +```python +from rcspp.graph import AlgorithmParams + +p = AlgorithmParams() +p.stop_after_X_solutions = 1 # stop after N solutions +p.max_iterations = 1_000_000 +p.timeout_s = 60.0 +p.num_max_phases = 1 +p.seed = 0 +p.tabu_tenure = 5 +p.tabu_random_noise = True +p.forbidden_tabu = {source_id, sink_id} +p.limit_to_available_ram = False +p.memory_limit_fraction = 0.9 +p.release_after_solve = True +``` + +## `BucketAlgorithmParams` + +```python +from rcspp.graph import BucketAlgorithmParams + +bp = BucketAlgorithmParams() +bp.range_buckets = 100 # number of buckets +bp.bucket_resource_index = 0 # which resource partitions the buckets +bp.sort_resource_index = 1 # sort key within each bucket +# All AlgorithmParams fields are also available +``` + +--- + +## `SolveResult` + +```python +result = rg.solve() + +result.solutions # list[Solution], sorted best-first +result.status # AlgorithmStatus enum +result.status_string() # "complete" | "timeout" | "max_solutions" | … +result.num_extended_labels # int + +for sol in result: # iterable + print(sol.cost) + +result[0] # index access +result[-1] # negative index +bool(result) # True if any solutions +repr(result) # "SolveResult(n=3, status=complete)" +``` + +### `Solution` + +```python +sol.cost # float +sol.path_node_ids # list[int] +sol.path_arc_ids # list[int] +sol.column # Column object + +# numpy helpers (requires numpy) +cost, nodes, row_idx, row_coeff = sol.to_arrays() +``` + +### `Column` and `Row` + +```python +from rcspp._core.graph import Column, Row + +col = Column() +col.cost = 10.0 +col.rows = [Row(index=0, coefficient=1.0)] +``` + +--- + +## Resource Functions + +All classes live in `rcspp.resource`. The generic descriptors (e.g. +`AdditionExtensionFunction`) are automatically resolved to the correct typed +C++ class when `add__resource` is called. + +### Extension functions + +| Class | Applies to | Effect | +|---|---|---| +| `AdditionExtensionFunction()` | Numerical | `result = current + arc_value` | +| `SubtractExtensionFunction()` | Numerical | `result = current - arc_value` | +| `TimeWindowExtensionFunction(tw, default_max=None)` | Numerical | `result = max(current + travel, ready[node])` | +| `UnionExtensionFunction()` | Container | `result = current ∪ arc_value` | +| `IntersectionExtensionFunction()` | Container | `result = current ∩ arc_value` | +| `SubtractExtensionFunction()` | Container | `result = current − arc_value` | +| `NGPathExtensionFunction()` | Set | Adds destination node to visited set | + +`tw` is a `dict` mapping `node_id → (ready_time, due_time)`. + +### Feasibility functions + +| Class | Condition | +|---|---| +| `TrivialFeasibilityFunction()` | Always feasible | +| `MinMaxFeasibilityFunction(min, max)` | `min ≤ value ≤ max` | +| `TimeWindowFeasibilityFunction(tw)` | `value ≤ due_time[node]` | +| `SizeFeasibilityFunction(min_size, max_size)` | `min ≤ len(container) ≤ max` | +| `IntersectionFeasibilityFunction()` | `current ∩ arc_value ≠ ∅` | +| `ReachableFeasibilityFunction()` | Can reach sink | + +### Dominance functions + +| Class | Condition | +|---|---| +| `TrivialDominanceFunction()` | Never dominates (keep all) | +| `ValueDominanceFunction()` | `lhs.value ≤ rhs.value` | +| `InclusionDominanceFunction()` | `lhs ⊆ rhs` | +| `ContainDominanceFunction()` | `lhs ⊇ rhs` | + +### Cost functions + +| Class | Returns | +|---|---| +| `TrivialCostFunction()` | `0.0` | +| `ValueCostFunction()` | `float(resource.value)` | + +--- + +## `PricingPool` (column generation) + +`PricingPool` bridges the C++ `SolutionPool` and a cross-process numpy shared +array, enabling lock-free pricing in worker processes. + +```python +from rcspp.pricing_pool import PricingPool +import numpy as np + +pool = PricingPool(n_constraints=200, max_cols=50_000) + +# Add a solution (deduplicates by arc path) +col_id = pool.add(solution) + +# Batch add +col_ids = pool.add_columns([sol1, sol2, sol3]) + +# Price: returns (ColumnIds, reduced_costs) sorted best-first +ids, rcs = pool.price(duals=np.zeros(200), threshold=-1e-9) + +# Activity tracking (call after LP basis is updated) +pool.update_activity(basis_col_ids) # forwarded to C++ pool + +# Create a view (for B&B nodes) +sub = pool.new_filter(forbidden_arc_ids=[10, 11], max_age=100) +ids, rcs = sub.price(duals) + +# Cleanup +pool.remove_stale(max_age=50, min_usage_rate=0.01) +pool.close() +``` + +### Cross-process usage (parallel pricing) + +```python +# Master process +pool = PricingPool(n_constraints=200, max_cols=50_000) +handle = pool.handle() # serialisable dict + +# Worker process +from rcspp.pricing_pool import PricingPool +shared = PricingPool.attach(handle) # SharedPricingPool (numpy only, no C++) +indices, rcs = shared.price(duals) # lock-free; returns shared slot indices +``` + +--- + +## Logging + +```python +from rcspp import LogLevel, set_log_level, get_log_level, init_logger + +set_log_level(LogLevel.Debug) # Debug | Info | Warning | Error | Off +init_logger(LogLevel.Info, to_console=True, file_path="rcspp.log") + +current = get_log_level() +``` + +--- + +## Memory utilities + +```python +from rcspp import process_memory_bytes, available_memory_bytes + +rss = process_memory_bytes() # current RSS in bytes +available = available_memory_bytes() # available system RAM in bytes +``` diff --git a/docs/python/index.md b/docs/python/index.md new file mode 100644 index 00000000..109a86b3 --- /dev/null +++ b/docs/python/index.md @@ -0,0 +1,45 @@ +--- +title: Python Package +nav_order: 4 +has_children: true +--- + +# Python Package + +The Python package `rcspp` wraps the C++ core via pybind11 and adds a +Pythonic layer for ease of use. + +## Package layout + +```text +rcspp/ +├── __init__.py # top-level imports: LogLevel, set_log_level, … +├── graph.py # ResourceGraph, AlgorithmParams, BucketAlgorithmParams +├── resource.py # Extension / feasibility / cost / dominance functions +├── pricing_pool.py # PricingPool, FilteredPricingPool, SharedPricingPool +├── logger.py # Logging helpers +└── _core/ # Compiled C++ extension (auto-discovered at import) + ├── graph # SolveResult, Solution, Column, Row, AlgorithmParams, … + ├── resource # Typed C++ function classes + └── solution_pool # SolutionPool, FilteredSolutionPool +``` + +## Import style + +```python +# High-level Python wrappers (recommended) +from rcspp.graph import ResourceGraph, AlgorithmParams, BucketAlgorithmParams +from rcspp.resource import ( + AdditionExtensionFunction, MinMaxFeasibilityFunction, + ValueCostFunction, ValueDominanceFunction, TrivialFeasibilityFunction, + TrivialCostFunction, TimeWindowExtensionFunction, + TimeWindowFeasibilityFunction, UnionExtensionFunction, + IntersectionExtensionFunction, InclusionDominanceFunction, + ContainDominanceFunction, SizeFeasibilityFunction, +) +from rcspp.pricing_pool import PricingPool + +# Low-level C++ objects (advanced use) +from rcspp._core.graph import Solution, Column, Row, Algorithm +from rcspp._core.solution_pool import SolutionPool +``` diff --git a/docs/python/quickstart.md b/docs/python/quickstart.md new file mode 100644 index 00000000..d6a977ea --- /dev/null +++ b/docs/python/quickstart.md @@ -0,0 +1,186 @@ +--- +title: Python Quick Start +parent: Python Package +nav_order: 1 +--- + +# Python Quick Start + +## Installation + +```bash +pip install rcspp +``` + +## Minimal example + +```python +from rcspp.graph import ResourceGraph +from rcspp.resource import ( + AdditionExtensionFunction, + MinMaxFeasibilityFunction, + ValueCostFunction, + ValueDominanceFunction, +) + +rg = ResourceGraph() + +# One real resource: distance, must stay ≤ 50 +rg.add_real_resource( + AdditionExtensionFunction(), + MinMaxFeasibilityFunction(0.0, 50.0), + ValueCostFunction(), + ValueDominanceFunction(), +) + +rg.add_node(0, source=True) +rg.add_node(1) +rg.add_node(2) +rg.add_node(3, sink=True) + +# add_arc(resource_consumption_tuple, origin, dest, cost) +rg.add_arc((10.0,), 0, 1, cost=10.0) +rg.add_arc((15.0,), 1, 3, cost=15.0) +rg.add_arc((20.0,), 0, 2, cost=20.0) +rg.add_arc((10.0,), 2, 3, cost=10.0) + +result = rg.solve() +print(result.solutions[0].cost) # 25.0 +print(result.solutions[0].path_node_ids) # [0, 1, 3] +print(result.status_string()) # "complete" +``` + +--- + +## Two resources — time windows + capacity + +```python +from rcspp.graph import ResourceGraph +from rcspp.resource import ( + TimeWindowExtensionFunction, TimeWindowFeasibilityFunction, ValueDominanceFunction, + AdditionExtensionFunction, MinMaxFeasibilityFunction, + ValueCostFunction, TrivialCostFunction, +) + +# time windows: {node_id: (ready_time, due_time)} +tw = {0: (0.0, 0.0), 1: (5.0, 20.0), 2: (0.0, 100.0)} + +rg = ResourceGraph() + +# Resource 0: time (objective = total time) +rg.add_real_resource( + TimeWindowExtensionFunction(tw), + TimeWindowFeasibilityFunction(tw), + ValueCostFunction(), + ValueDominanceFunction(), +) + +# Resource 1: demand / capacity ≤ 10 +rg.add_int_resource( + AdditionExtensionFunction(), + MinMaxFeasibilityFunction(0, 10), + TrivialCostFunction(), + ValueDominanceFunction(), +) + +rg.add_node(0, source=True) +rg.add_node(1) +rg.add_node(2, sink=True) + +# arc: (time, demand) +rg.add_arc((5.0, 3), 0, 1, cost=5.0) +rg.add_arc((8.0, 4), 1, 2, cost=8.0) + +result = rg.solve() +``` + +--- + +## From a NetworkX graph + +```python +import networkx as nx +from rcspp.graph import ResourceGraph +from rcspp.resource import AdditionExtensionFunction, TrivialFeasibilityFunction, \ + ValueCostFunction, ValueDominanceFunction + +G = nx.DiGraph() +G.add_node(0, source=True) +G.add_node(1) +G.add_node(2, sink=True) +G.add_edge(0, 1, cost=3.0, consumption=(3.0,)) +G.add_edge(1, 2, cost=5.0, consumption=(5.0,)) + +rg = ResourceGraph(G) +rg.add_real_resource( + AdditionExtensionFunction(), + TrivialFeasibilityFunction(), + ValueCostFunction(), + ValueDominanceFunction(), +) +result = rg.solve() +``` + +--- + +## Multiple solutions (diversification) + +```python +from rcspp._core.graph import AlgorithmParams, Algorithm + +params = AlgorithmParams() +params.stop_after_X_solutions = 10 +params.max_iterations = 100 +params.seed = 42 + +result = rg.solve(algorithm="greedy", params=params) +for sol in result.solutions: + print(sol.cost, sol.path_node_ids) +``` + +--- + +## Column generation loop + +```python +import math + +rg = ResourceGraph() +# … build graph, add rows to arcs … +# rg.add_arc((10.0,), 0, 1, cost=10.0, rows=[(constraint_idx, coefficient)]) + +duals = [0.0] * n_constraints + +while True: + rg.update_reduced_costs(duals, cost_index=0) + result = rg.solve(upper_bound=-1e-9) + + if not result.solutions: + break # no negative reduced-cost column + + for sol in result.solutions: + master.add_column(sol.column) + + duals = master.get_duals() +``` + +--- + +## Set-based resource (NG-paths) + +```python +from rcspp.resource import ( + NGPathExtensionFunction, SizeFeasibilityFunction, + TrivialCostFunction, InclusionDominanceFunction, +) + +rg = ResourceGraph() + +# Track visited nodes (set); no two paths that visit the same node dominate +rg.add_int_set_resource( + NGPathExtensionFunction(), + SizeFeasibilityFunction(0, 10), # path length ≤ 10 nodes + TrivialCostFunction(), + InclusionDominanceFunction(), # L1 dominates L2 if visited(L1) ⊆ visited(L2) +) +``` From bd85314aa0665349bf4c1148e22b826f4d5c83e4 Mon Sep 17 00:00:00 2001 From: legraina Date: Thu, 11 Jun 2026 17:18:21 +0200 Subject: [PATCH 02/10] fix(test): skip test_cg_tiny gracefully when MIP solver unavailable On Windows, mip can import but crash with TypeError when Gurobi DLL lookup returns None. Broaden the exception guard to catch all import errors, and wrap vrp.solve() in a try/except so any solver-init failure also results in a proper pytest.skip rather than an ERROR. Co-Authored-By: Claude Sonnet 4.6 --- tests/python/test_vrp.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/python/test_vrp.py b/tests/python/test_vrp.py index 47db18ae..e9ff852c 100644 --- a/tests/python/test_vrp.py +++ b/tests/python/test_vrp.py @@ -206,13 +206,19 @@ def test_cg_tiny(): customers.""" try: import mip # noqa: F401 - except ImportError: - print(" [skip] mip not installed") - return + except Exception: + import pytest + + pytest.skip("mip not available on this platform") inst = make_tiny_instance() vrp = VRP(inst) - mp_sol = vrp.solve() + try: + mp_sol = vrp.solve() + except Exception as exc: + import pytest + + pytest.skip(f"MIP solver unavailable: {exc}") assert mp_sol.cost > 0, f"IP cost must be positive, got {mp_sol.cost}" From 435060216f66cc7e82b5ef4ea0ed388f35009279 Mon Sep 17 00:00:00 2001 From: legraina Date: Thu, 11 Jun 2026 17:54:45 +0200 Subject: [PATCH 03/10] fix(test): skip test_cg_tiny on Windows before importing mip mip's Gurobi auto-detection creates internal state (Model.__del__) that calls LoadLibrary(None) during teardown when no Gurobi license is present. Catching the exception in the test body does not prevent the teardown crash. Skip the entire test on sys.platform == 'win32' so mip is never imported and no Gurobi state is created. Co-Authored-By: Claude Sonnet 4.6 --- tests/python/test_vrp.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/python/test_vrp.py b/tests/python/test_vrp.py index e9ff852c..e2200e4a 100644 --- a/tests/python/test_vrp.py +++ b/tests/python/test_vrp.py @@ -204,11 +204,18 @@ def test_c101_5_subproblem(): def test_cg_tiny(): """Full column generation on the tiny instance converges and covers all customers.""" + import pytest + + # On Windows, mip auto-detects Gurobi via a registry path that may be None. + # LoadLibrary(None) / NoneType-iteration errors then surface in __del__ + # (teardown) rather than at import time, so we must skip before importing + # mip at all. + if sys.platform == "win32": + pytest.skip("mip/Gurobi not available on Windows CI runners") + try: import mip # noqa: F401 except Exception: - import pytest - pytest.skip("mip not available on this platform") inst = make_tiny_instance() @@ -216,8 +223,6 @@ def test_cg_tiny(): try: mp_sol = vrp.solve() except Exception as exc: - import pytest - pytest.skip(f"MIP solver unavailable: {exc}") assert mp_sol.cost > 0, f"IP cost must be positive, got {mp_sol.cost}" From 2e306dc660454ea4a3aa5618d5e1585934bea971 Mon Sep 17 00:00:00 2001 From: legraina Date: Thu, 11 Jun 2026 20:07:06 +0200 Subject: [PATCH 04/10] fix(test): exclude mip from Windows installs; broaden Gurobi teardown filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mip's Gurobi DLL probe runs at import time on Windows and crashes __del__ with TypeError (LoadLibrary/NoneType) even when the test is skipped. Two-pronged fix: 1. requirements.txt: add sys_platform != 'win32' so mip is never installed on Windows runners — nothing to clean up at teardown. 2. conftest.py: widen the PytestUnraisableExceptionWarning filter to cover all mip/Gurobi-related patterns (SolverGurobi, LoadLibrary, NoneType-iterable, gurobipy) as a belt-and-suspenders guard. Co-Authored-By: Claude Sonnet 4.6 --- tests/python/conftest.py | 25 +++++++++++++++++-------- tests/python/requirements.txt | 2 +- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/tests/python/conftest.py b/tests/python/conftest.py index 50aa2ce4..9c2e69cb 100644 --- a/tests/python/conftest.py +++ b/tests/python/conftest.py @@ -21,14 +21,23 @@ def _flush_gcov_on_exit(): def pytest_configure(config): - # mip's SolverGurobi.__del__ crashes with AttributeError when Gurobi is not - # licensed: __init__ raises before setting _ownsModel, then __del__ accesses it. - # This is an upstream mip bug; suppress the resulting unraisable-exception warning. - warnings.filterwarnings( - "ignore", - message=".*SolverGurobi.*", - category=pytest.PytestUnraisableExceptionWarning, - ) + # mip's Gurobi backend crashes in __del__ when Gurobi is not licensed: + # SolverGurobi.__init__ raises before setting _ownsModel, then __del__ + # accesses it (AttributeError); on Windows, LoadLibrary(None) or a + # NoneType-path check raises TypeError. All of these surface as + # PytestUnraisableExceptionWarning — suppress any that mention mip or + # Gurobi, and also the LoadLibrary / NoneType variants from the DLL probe. + for _pat in ( + r".*SolverGurobi.*", + r".*LoadLibrary.*", + r".*NoneType.*iterable.*", + r".*gurobipy.*", + ): + warnings.filterwarnings( + "ignore", + message=_pat, + category=pytest.PytestUnraisableExceptionWarning, + ) # pytest's internal cache plugin leaves sqlite3 connections open; suppress the # resulting ResourceWarning so it doesn't pollute test output. warnings.filterwarnings( diff --git a/tests/python/requirements.txt b/tests/python/requirements.txt index 978634d0..2b7644fd 100644 --- a/tests/python/requirements.txt +++ b/tests/python/requirements.txt @@ -1,4 +1,4 @@ -mip +mip; sys_platform != "win32" networkx pytest pytest-cov From 90648f39329ffc6b29641a46cb7adec420c7bd33 Mon Sep 17 00:00:00 2001 From: legraina Date: Thu, 11 Jun 2026 20:16:55 +0200 Subject: [PATCH 05/10] fix(test): catch TypeError/OSError in gcov flush fixture on Windows ctypes.CDLL(None) is valid on Linux/macOS (loads libc) but raises TypeError: LoadLibrary() argument 1 must be str, not None on Windows. The _flush_gcov_on_exit fixture only caught AttributeError, so the TypeError propagated as an ERROR at teardown of the last test. Co-Authored-By: Claude Sonnet 4.6 --- tests/python/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/python/conftest.py b/tests/python/conftest.py index 9c2e69cb..f1b2267a 100644 --- a/tests/python/conftest.py +++ b/tests/python/conftest.py @@ -16,8 +16,8 @@ def _flush_gcov_on_exit(): yield try: ctypes.CDLL(None).__gcov_dump() - except AttributeError: - pass # not a coverage build or __gcov_dump not exported + except (AttributeError, TypeError, OSError): + pass # not a coverage build, __gcov_dump not exported, or non-Unix platform def pytest_configure(config): From 0714d0d16b26d43d4b1305ac5a15199266e79e19 Mon Sep 17 00:00:00 2001 From: legraina Date: Thu, 11 Jun 2026 21:31:44 +0200 Subject: [PATCH 06/10] fix(ci): increase gtest discovery timeout; add /run-ci PR comment trigger - gtest_discover_tests: bump DISCOVERY_TIMEOUT from 5s to 30s so Debug binaries on Windows (slow DLL load) don't time out during test list discovery at build time. - ci-full.yml: add issue_comment trigger so posting '/run-ci' in any PR comment launches the full matrix. Jobs guard on workflow_dispatch OR (PR comment AND '/run-ci' body). Checkout uses refs/pull/N/head when triggered by a comment. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci-full.yml | 17 ++++++++++++++--- tests/cpp/CMakeLists.txt | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-full.yml b/.github/workflows/ci-full.yml index 976c4ab3..a3c05496 100644 --- a/.github/workflows/ci-full.yml +++ b/.github/workflows/ci-full.yml @@ -1,11 +1,12 @@ name: CI Full # Full test matrix (all OS × build types × Python versions) plus wheel/sdist builds. -# One-click via "Run workflow" in GitHub UI — also calls the coverage gate so -# everything runs together. The coverage gate also runs automatically on -# push/PR to main (ci.yml). +# Triggered manually ("Run workflow") or by posting `/run-ci` in any PR comment. +# The coverage gate also runs automatically on push/PR to main (ci.yml). on: workflow_dispatch: + issue_comment: + types: [created] env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true @@ -18,10 +19,18 @@ jobs: # ── Coverage gate (reuses ci.yml definition — no duplication) ───────────── coverage: name: Coverage Gate + if: > + github.event_name == 'workflow_dispatch' || + (github.event.issue.pull_request != null && + contains(github.event.comment.body, '/run-ci')) uses: ./.github/workflows/ci.yml test: name: Test / ${{ matrix.os }} / ${{ matrix.build-type }} / py${{ matrix.python-version }} + if: > + github.event_name == 'workflow_dispatch' || + (github.event.issue.pull_request != null && + contains(github.event.comment.body, '/run-ci')) runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -43,6 +52,8 @@ jobs: with: submodules: recursive lfs: true + # When triggered by a PR comment, check out the PR head branch + ref: ${{ github.event_name == 'issue_comment' && format('refs/pull/{0}/head', github.event.issue.number) || github.sha }} - name: Install dependencies (Linux) if: runner.os == 'Linux' diff --git a/tests/cpp/CMakeLists.txt b/tests/cpp/CMakeLists.txt index 60380d76..5fdacb8e 100644 --- a/tests/cpp/CMakeLists.txt +++ b/tests/cpp/CMakeLists.txt @@ -40,4 +40,4 @@ if(USE_COVERAGE) endif() include(GoogleTest) -gtest_discover_tests(${PROJECT_NAME}) +gtest_discover_tests(${PROJECT_NAME} DISCOVERY_TIMEOUT 30) From 5020c4f206394ac80d58742b4a0a17ec1a9bb097 Mon Sep 17 00:00:00 2001 From: legraina Date: Thu, 11 Jun 2026 21:57:04 +0200 Subject: [PATCH 07/10] fix(docs): add missing just-the-docs gem dependencies to Gemfile jekyll-seo-tag and jekyll-include-cache are required by just-the-docs but were absent from the Gemfile, causing local jekyll build to fail. Co-Authored-By: Claude Sonnet 4.6 --- docs/Gemfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/Gemfile b/docs/Gemfile index 43766a9b..a1edebda 100644 --- a/docs/Gemfile +++ b/docs/Gemfile @@ -2,4 +2,6 @@ source "https://rubygems.org" gem "jekyll", "~> 4.3" gem "jekyll-remote-theme" +gem "jekyll-seo-tag", ">= 2.0" +gem "jekyll-include-cache" gem "webrick" From 5ab59937008b1dd2ce2ef5a7bbf3f4685dce0a8b Mon Sep 17 00:00:00 2001 From: legraina Date: Thu, 11 Jun 2026 23:23:21 +0200 Subject: [PATCH 08/10] docs: migrate to Sphinx/Furo with full Doxygen + autodoc API reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace Jekyll/just-the-docs with Sphinx + Furo theme - Add Doxygen XML pipeline (Doxyfile → Breathe/Exhale) for C++ API - Add sphinx.ext.autodoc + napoleon for Python API - Split C++ API into Beginner (instantiate) and Advanced (subclass) pages - Add Doxygen /// comments to all 57 previously undocumented C++ headers - Add Google-style docstrings to all Python modules - Update pages.yml to build Sphinx instead of Jekyll - Add .gitignore entries for _doxygen/, _site/, cpp/api/ (generated) - Remove obsolete Jekyll files (Gemfile, _config.yml, hand-written api.md files) Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/pages.yml | 28 +- .gitignore | 5 + cpp/rcspp/algorithm/solution.hpp | 84 ++++- cpp/rcspp/general/clonable.hpp | 20 + cpp/rcspp/graph/arc.hpp | 50 +++ cpp/rcspp/graph/node.hpp | 46 +++ cpp/rcspp/graph/row.hpp | 22 +- cpp/rcspp/label/label.hpp | 87 ++++- cpp/rcspp/label/label_factory.hpp | 34 ++ .../preprocessor/feasibility_preprocessor.hpp | 15 + cpp/rcspp/preprocessor/preprocessor.hpp | 36 ++ .../shortest_path_connectivity_sort.hpp | 37 ++ .../shortest_path_preprocessor.hpp | 23 ++ cpp/rcspp/rcspp.hpp | 14 + cpp/rcspp/resource/base/extender.hpp | 50 ++- .../resource/base/extender_prototype.hpp | 49 +++ cpp/rcspp/resource/base/resource.hpp | 92 ++++- .../resource/base/resource_prototype.hpp | 132 ++++++- cpp/rcspp/resource/base/resource_type.hpp | 6 + .../resource/composition/composition.hpp | 342 ++++++++++++++++-- .../composition/extender_composition.hpp | 32 +- .../cost/component_cost_function.hpp | 14 + .../cost/composition_cost_function.hpp | 10 + .../composition_dominance_function.hpp | 15 + .../composition_extension_function.hpp | 22 ++ ...achable_composition_extension_function.hpp | 21 ++ .../composition_feasibility_function.hpp | 24 ++ ...hable_composition_feasibility_function.hpp | 23 ++ .../composition/resource_composition.hpp | 119 +++++- .../resource_composition_factory.hpp | 83 ++++- .../composition/resource_type_composition.hpp | 34 +- .../resource/concrete/container_resource.hpp | 243 ++++++++++++- .../functions/cost/value_cost_function.hpp | 13 + .../dominance/contain_dominance_function.hpp | 32 ++ .../inclusion_dominance_function.hpp | 32 ++ .../dominance/value_dominance_function.hpp | 26 ++ .../extension/addition_extension_function.hpp | 18 + .../intersection_extension_function.hpp | 14 + .../extension/ng-path_extension_function.hpp | 49 ++- .../extension/subtract_extension_function.hpp | 14 + .../time_window_extension_function.hpp | 34 ++ .../extension/union_extension_function.hpp | 14 + .../intersection_feasibility_function.hpp | 28 ++ .../min_max_feasibility_function.hpp | 43 +++ .../reachable_feasibility_function.hpp | 28 ++ .../feasibility/size_feasibility_function.hpp | 33 ++ .../time_window_feasibility_function.hpp | 40 ++ .../resource/concrete/numerical_resource.hpp | 89 ++++- .../resource/functions/cost/cost_function.hpp | 50 +++ .../functions/cost/trivial_cost_function.hpp | 11 + .../dominance/dominance_function.hpp | 66 ++++ .../dominance/trivial_dominance_function.hpp | 18 + .../extension/extension_function.hpp | 65 ++++ .../extension/trivial_extension_function.hpp | 11 + .../feasibility/feasibility_function.hpp | 91 +++++ .../trivial_feasibility_function.hpp | 15 + cpp/rcspp/utils/logger.hpp | 96 ++++- cpp/rcspp/utils/timer.hpp | 73 +++- cpp/rcspp/utils/utils.hpp | 19 + docs/Doxyfile | 67 ++++ docs/Gemfile | 7 - docs/_config.yml | 29 -- docs/_static/custom.css | 29 ++ docs/advanced/algorithms.md | 2 - docs/advanced/column-generation.md | 2 - docs/advanced/index.md | 12 +- docs/conf.py | 132 +++++++ docs/cpp/advanced_api.md | 104 ++++++ docs/cpp/api.md | 311 ---------------- docs/cpp/beginner_api.md | 199 ++++++++++ docs/cpp/concepts.md | 2 - docs/cpp/index.md | 13 +- docs/cpp/quickstart.md | 2 - docs/index.md | 13 +- docs/python/api.md | 328 ----------------- docs/python/api.rst | 49 +++ docs/python/index.md | 10 +- docs/python/quickstart.md | 2 - docs/requirements.txt | 6 + python/src/rcspp/__init__.py | 7 + python/src/rcspp/_resource_types.py | 18 +- python/src/rcspp/graph.py | 121 +++++++ python/src/rcspp/logger.py | 22 +- python/src/rcspp/pricing_pool.py | 30 +- python/src/rcspp/resource.py | 268 +++++++++++++- 85 files changed, 3809 insertions(+), 810 deletions(-) create mode 100644 docs/Doxyfile delete mode 100644 docs/Gemfile delete mode 100644 docs/_config.yml create mode 100644 docs/_static/custom.css create mode 100644 docs/conf.py create mode 100644 docs/cpp/advanced_api.md delete mode 100644 docs/cpp/api.md create mode 100644 docs/cpp/beginner_api.md delete mode 100644 docs/python/api.md create mode 100644 docs/python/api.rst create mode 100644 docs/requirements.txt diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 1916cd95..47d4f70a 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -5,6 +5,9 @@ on: branches: [main] paths: - 'docs/**' + - 'cpp/rcspp/**' + - 'python/src/rcspp/**' + - 'python/bindings/**' - 'AGENT.md' workflow_dispatch: @@ -25,20 +28,29 @@ jobs: - uses: actions/configure-pages@v5 - - uses: ruby/setup-ruby@v1 + - name: Install Doxygen + run: sudo apt-get update -q && sudo apt-get install -y doxygen + + - uses: actions/setup-python@v5 with: - ruby-version: '3.3' - bundler-cache: true - working-directory: docs + python-version: '3.13' + + - name: Install Python dependencies + run: pip install -r docs/requirements.txt + + - name: Generate Doxygen XML + working-directory: docs + run: doxygen Doxyfile - - name: Build with Jekyll - run: bundle exec jekyll build --source docs --destination _site + - name: Build Sphinx site + working-directory: docs + run: sphinx-build -b html . _site -W --keep-going env: - JEKYLL_ENV: production + SPHINXOPTS: "-j auto" - uses: actions/upload-pages-artifact@v3 with: - path: _site + path: docs/_site deploy: environment: diff --git a/.gitignore b/.gitignore index 702eb40a..2b4a0814 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,8 @@ Testing/ # Clion .idea/ + +# Sphinx / Doxygen generated +docs/_doxygen/ +docs/_site/ +docs/cpp/api/ diff --git a/cpp/rcspp/algorithm/solution.hpp b/cpp/rcspp/algorithm/solution.hpp index e596d5a4..4f7dad4b 100644 --- a/cpp/rcspp/algorithm/solution.hpp +++ b/cpp/rcspp/algorithm/solution.hpp @@ -15,17 +15,30 @@ namespace rcspp { -// 64-bit FNV-1a constants +/// @brief FNV-1a 64-bit offset basis constant. inline constexpr std::uint64_t FNV_OFFSET_BASIS = 14695981039346656037ULL; + +/// @brief FNV-1a 64-bit prime multiplier constant. inline constexpr std::uint64_t FNV_PRIME = 1099511628211ULL; + +/// @brief Number of bytes in a 64-bit integer, used to drive the FNV-1a byte loop. inline constexpr int FNV_NUM_BYTES_UINT64 = 8; + +/// @brief Byte-extraction mask used inside the FNV-1a mixing loop. inline constexpr std::uint64_t FNV_NUM_BITS_PER_BYTE_UINT64 = 0xFFU; + +/// @brief Number of bits per byte, used to shift out processed bytes in the FNV-1a loop. inline constexpr int FNV_NUM_BITS_PER_BYTE = 8; -// Hash the raw bytes of a 64-bit integer using FNV-1a -// FNV-1a is a simple, fast, noncryptographic hash designed for hash tables and checksums. -// It processes input byte-by-byte: initialize the hash to an offset basis, for each byte XOR the -// hash with the byte, then multiply by a large prime (modulo the word size). +/// @brief Mixes a 64-bit value @p v into an FNV-1a running hash @p h. +/// +/// Processes @p v byte-by-byte using the FNV-1a algorithm: XOR each byte into +/// the running hash, then multiply by @c FNV_PRIME. This is a simple, fast, +/// non-cryptographic hash suitable for hash tables and checksums. +/// +/// @param v The 64-bit value to mix into the hash. +/// @param h The running hash state (defaults to @c FNV_OFFSET_BASIS for a fresh hash). +/// @return The updated hash after mixing all 8 bytes of @p v. static std::uint64_t fnv1a_mix_uint64(std::uint64_t v, std::uint64_t h = FNV_OFFSET_BASIS) { for (int i = 0; i < FNV_NUM_BYTES_UINT64; ++i) { // process 1 byte (8 bits) 8 times (64 bits) auto byte = @@ -37,8 +50,24 @@ static std::uint64_t fnv1a_mix_uint64(std::uint64_t v, std::uint64_t h = FNV_OFF return h; } +/// @brief Represents a feasible solution (path) found by the RCSPP algorithm. +/// +/// A @c Solution records the total path cost, the ordered sequence of node IDs, +/// the ordered sequence of arc IDs, and an optional master-LP @c Column. It +/// maintains an FNV-1a hash of the arc-ID sequence for fast equality testing +/// inside @c std::unordered_set. struct Solution { + /// @brief Constructs a default, infeasible-sentinel solution. + /// + /// @c cost is initialised to @c +infinity and the path vectors are empty. Solution() noexcept { init_hash(); } + + /// @brief Constructs a fully specified solution. + /// + /// @param _cost Total path cost (sum of arc costs). + /// @param _path_node_ids Ordered sequence of node IDs along the path. + /// @param _path_arc_ids Ordered sequence of arc IDs along the path. + /// @param _column Optional master-LP column associated with this solution. Solution(double _cost, std::vector _path_node_ids, std::vector _path_arc_ids, Column _column = {}) : cost(_cost), @@ -48,27 +77,43 @@ struct Solution { init_hash(); } - // Hash equality is used as a fast prefilter (short-circuits the cheap path); - // on a match we still compare the arc paths so genuine FNV collisions don't - // silently coalesce distinct solutions in the unordered_set used by - // extract_solution. + /// @brief Compares two solutions for equality. + /// + /// The hash serves as a fast prefilter: only when hashes match is the full + /// arc-ID sequence compared, so genuine FNV-1a collisions do not coalesce + /// distinct solutions in @c std::unordered_set. + /// + /// @param rhs The solution to compare against. + /// @return @c true if both solutions traverse exactly the same sequence of arcs. bool operator==(const Solution& rhs) const noexcept { return hash_ == rhs.hash_ && path_arc_ids == rhs.path_arc_ids; } + /// @brief Returns the FNV-1a hash of the arc-ID sequence. + /// + /// @return The 64-bit content hash used by the @c std::hash specialisation. [[nodiscard]] uint64_t get_hash() const noexcept { return hash_; } - // Recompute the content hash from path_arc_ids. The value constructor hashes automatically; - // call this after mutating path_arc_ids directly (the Python path_arc_ids setter does). + /// @brief Recomputes the content hash after @c path_arc_ids has been mutated. + /// + /// The value constructor hashes automatically. Call this method when + /// @c path_arc_ids is modified directly (e.g., via the Python setter). void rehash() noexcept { init_hash(); } - // Path/route cost (sum of arc costs along the path). Distinct from column.cost — the - // master-LP column cost that SolutionPool prices on. The two are normally equal but are - // stored separately; this defaults to +inf (the RCSPP unset/infeasible sentinel), so a - // Solution built by setting only `column` leaves this field at +inf. + /// @brief Total cost of the path (sum of arc costs along the route). + /// + /// Distinct from @c column.cost, which is the master-LP reduced cost used by + /// the pricing step. The two are normally equal but stored separately. + /// Defaults to @c +infinity (the RCSPP infeasible/unset sentinel). double cost = std::numeric_limits::infinity(); + + /// @brief Ordered sequence of node IDs visited by the path. std::vector path_node_ids; + + /// @brief Ordered sequence of arc IDs traversed by the path. std::vector path_arc_ids; + + /// @brief Master-LP column associated with this solution (may be empty). Column column; private: @@ -86,8 +131,15 @@ struct Solution { }; } // namespace rcspp -// hash specialization for Solution for unordered_set +/// @brief @c std::hash specialisation for @c rcspp::Solution. +/// +/// Delegates to @c Solution::get_hash() so that @c Solution objects can be stored +/// directly in @c std::unordered_set and @c std::unordered_map. template <> struct std::hash { + /// @brief Computes the hash of a @c Solution. + /// + /// @param s The solution to hash. + /// @return The FNV-1a hash of the solution's arc-ID sequence. size_t operator()(rcspp::Solution const& s) const noexcept { return s.get_hash(); } }; diff --git a/cpp/rcspp/general/clonable.hpp b/cpp/rcspp/general/clonable.hpp index 4cc1a063..6e3b6821 100644 --- a/cpp/rcspp/general/clonable.hpp +++ b/cpp/rcspp/general/clonable.hpp @@ -8,9 +8,29 @@ namespace rcspp { +/// @brief CRTP mixin that provides a type-safe @c clone() implementation for polymorphic +/// hierarchies. +/// +/// Inherit from @c Clonable to automatically implement the @c clone() method declared in +/// @p BaseType. The returned @c unique_ptr always points to a freshly copy-constructed +/// @p DerivedType, preserving the full derived-class state. +/// +/// Example usage: +/// @code +/// class MyResource : public Clonable> { ... }; +/// @endcode +/// +/// @tparam DerivedType The concrete class that inherits from this mixin. Used for the +/// copy-construction. +/// @tparam BaseType The abstract base class that declares the virtual @c clone() method. +/// @tparam ReturnType The type returned by @c clone() (defaults to @p BaseType). Useful when the +/// base hierarchy uses a covariant return type different from @p BaseType. template class Clonable : public BaseType { public: + /// @brief Creates a deep copy of this object as the concrete @p DerivedType. + /// + /// @return A @c unique_ptr owning a newly copy-constructed @p DerivedType. [[nodiscard]] auto clone() const -> std::unique_ptr override { return std::make_unique(static_cast(*this)); } diff --git a/cpp/rcspp/graph/arc.hpp b/cpp/rcspp/graph/arc.hpp index 5e60c2e1..28082e27 100644 --- a/cpp/rcspp/graph/arc.hpp +++ b/cpp/rcspp/graph/arc.hpp @@ -17,10 +17,28 @@ namespace rcspp { +/// @brief Directed arc in an RCSPP graph connecting two nodes with a cost and optional resource +/// extender. +/// +/// Each arc carries a unique identifier, pointers to its origin and destination nodes, +/// an optional resource @c Extender that propagates label state along the arc, +/// a traversal cost, and a list of LP master-problem row contributions. +/// +/// @tparam ResourceType The resource type used by nodes and labels in this graph. +/// Must satisfy @c ResourceTypeConcept. template requires ResourceTypeConcept class Arc { public: + /// @brief Constructs an arc with a full set of attributes. + /// + /// @param arc_id Unique numeric identifier for this arc. + /// @param origin_node Pointer to the tail (origin) node. Must not be null. + /// @param destination_node Pointer to the head (destination) node. Must not be null. + /// @param arc_extender Owning pointer to the resource extender applied when traversing this + /// arc. + /// @param arc_cost Traversal cost associated with this arc. + /// @param rows LP master-problem row contributions for this arc. Arc(size_t arc_id, Node* origin_node, Node* destination_node, std::unique_ptr> arc_extender, double arc_cost, std::vector rows = {}) @@ -31,26 +49,51 @@ class Arc { cost(arc_cost), rows(std::move(rows)) {} + /// @brief Constructs an arc without a resource extender. + /// + /// @param arc_id Unique numeric identifier for this arc. + /// @param origin_node Pointer to the tail (origin) node. Must not be null. + /// @param destination_node Pointer to the head (destination) node. Must not be null. + /// @param arc_cost Traversal cost associated with this arc. + /// @param rows LP master-problem row contributions for this arc. Arc(size_t arc_id, Node* origin_node, Node* destination_node, double arc_cost, std::vector rows = {}) : Arc(arc_id, origin_node, destination_node, nullptr, arc_cost, std::move(rows)) {} + /// @brief Constructs a zero-cost arc without a resource extender. + /// + /// @param arc_id Unique numeric identifier for this arc. + /// @param origin_node Pointer to the tail (origin) node. Must not be null. + /// @param destination_node Pointer to the head (destination) node. Must not be null. + /// @param rows LP master-problem row contributions for this arc. Arc(size_t arc_id, Node* origin_node, Node* destination_node, std::vector rows = {}) : Arc(arc_id, origin_node, destination_node, 0, std::move(rows)) {} + /// @brief Unique numeric identifier for this arc. const size_t id; + /// @brief Pointer to the tail (origin) node of this arc. Node* const origin; + /// @brief Pointer to the head (destination) node of this arc. Node* const destination; + /// @brief Optional resource extender applied when a label traverses this arc. + /// + /// May be null if no resource extension is needed. std::unique_ptr> extender; + /// @brief Traversal cost of this arc. double cost; + /// @brief LP master-problem row contributions associated with this arc. std::vector rows; + /// @brief Returns a human-readable string representation of this arc. + /// + /// @return A string describing the arc id, origin, destination, cost, and extender (if + /// present). [[nodiscard]] std::string to_string() const { std::stringstream ss; ss << "Arc(id=" << id << ", origin=" << origin->id @@ -62,6 +105,13 @@ class Arc { return ss.str(); } }; + +/// @brief Writes a human-readable representation of @p arc to the output stream @p os. +/// +/// @tparam ResourceType The resource type used by the arc. +/// @param os The output stream to write to. +/// @param arc The arc to serialize. +/// @return The same output stream @p os, to allow chaining. template std::ostream& operator<<(std::ostream& os, const Arc& arc) { return os << arc.to_string(); diff --git a/cpp/rcspp/graph/node.hpp b/cpp/rcspp/graph/node.hpp index b073041b..f61d36a4 100644 --- a/cpp/rcspp/graph/node.hpp +++ b/cpp/rcspp/graph/node.hpp @@ -15,31 +15,66 @@ namespace rcspp { +/// @brief Forward declaration of Arc for use in Node. +/// +/// @tparam ResourceType The resource type used in the graph. template requires ResourceTypeConcept class Arc; +/// @brief Forward declaration of Graph for friendship in Node. +/// +/// @tparam ResourceType The resource type used in the graph. template requires ResourceTypeConcept class Graph; +/// @brief A vertex in an RCSPP graph, holding adjacency lists, an optional node resource, and +/// topology metadata. +/// +/// Each node has a unique identifier and flags indicating whether it acts as a source +/// or sink. Incoming and outgoing arcs are stored as non-owning raw pointers. +/// An optional @c Resource can be attached to enforce node-level resource constraints. +/// The sorted position @c pos() is only valid after @c Graph::sort_nodes() has been called. +/// +/// @tparam ResourceType The resource type used by arcs and labels in this graph. +/// Must satisfy @c ResourceTypeConcept. template requires ResourceTypeConcept class Node { public: + /// @brief Constructs a node with the given id and source/sink flags. + /// + /// @param node_id Unique numeric identifier for this node. + /// @param source True if this node is a source (path starting point). + /// @param sink True if this node is a sink (path ending point). explicit Node(size_t node_id, bool source, bool sink) : id(node_id), source(source), sink(sink) {} + /// @brief Unique numeric identifier for this node. const size_t id; + /// @brief Non-owning pointers to all arcs whose destination is this node. std::vector*> in_arcs; + + /// @brief Non-owning pointers to all arcs whose origin is this node. std::vector*> out_arcs; + /// @brief Optional resource attached to this node for node-level constraints. + /// + /// May be null if no node resource is needed. std::unique_ptr> resource; + /// @brief True if this node is a source (labels may start here). const bool source; + + /// @brief True if this node is a sink (labels may terminate here). const bool sink; + /// @brief Returns the topological position of this node in the sorted graph. + /// + /// @return The zero-based position index assigned by @c Graph::sort_nodes(). + /// @throws std::bad_optional_access If @c Graph::sort_nodes() has not been called yet. [[nodiscard]] size_t pos() const { try { return pos_.value(); @@ -51,6 +86,10 @@ class Node { } } + /// @brief Returns a human-readable string representation of this node. + /// + /// @return A string describing the node id, source/sink flags, attached resource, + /// and the ids of predecessor and successor nodes. [[nodiscard]] std::string to_string() const { std::stringstream ss; ss << "Node(id=" << id; @@ -85,6 +124,13 @@ class Node { size_t csr_in_start_{0}; size_t csr_in_count_{0}; }; + +/// @brief Writes a human-readable representation of @p node to the output stream @p os. +/// +/// @tparam ResourceType The resource type used by the node. +/// @param os The output stream to write to. +/// @param node The node to serialize. +/// @return The same output stream @p os, to allow chaining. template std::ostream& operator<<(std::ostream& os, const Node& node) { return os << node.to_string(); diff --git a/cpp/rcspp/graph/row.hpp b/cpp/rcspp/graph/row.hpp index 2ff604c6..374549aa 100644 --- a/cpp/rcspp/graph/row.hpp +++ b/cpp/rcspp/graph/row.hpp @@ -8,16 +8,32 @@ namespace rcspp { +/// @brief A single constraint-row entry used to build LP master-problem columns. +/// +/// Each @c Row records the index of an LP constraint row and the coefficient +/// that a path (column) contributes to that row. struct Row { + /// @brief Zero-based index of the LP constraint row. size_t index; + + /// @brief Coefficient contributed by this arc to the LP constraint row. long double coefficient; }; -// Represents a column in the LP master problem. -// cost: sum of original arc costs along the path (no dual contribution). -// rows: aggregated constraint coefficients (Row.coefficient summed per Row.index). +/// @brief Represents a column in the LP master problem generated by a path. +/// +/// The @c cost field holds the sum of original arc costs along the path with no +/// dual contribution. The @c rows field accumulates constraint coefficients keyed +/// by row index (Row::coefficient values are summed per Row::index across all arcs +/// of the path). struct Column { + /// @brief Sum of original arc costs along the path (no dual contribution). double cost = 0.0; + + /// @brief Aggregated LP constraint coefficients for this column. + /// + /// Each entry represents a unique constraint row and the total coefficient + /// that this path contributes to it. std::vector rows; }; diff --git a/cpp/rcspp/label/label.hpp b/cpp/rcspp/label/label.hpp index d9c364bb..28ae42a5 100644 --- a/cpp/rcspp/label/label.hpp +++ b/cpp/rcspp/label/label.hpp @@ -18,15 +18,28 @@ namespace rcspp { template class LabelFactory; +/// @brief Represents a label in the resource-constrained shortest-path label-setting algorithm. +/// +/// A label encodes the state of a partial path: the accumulated resource consumption, +/// the current node at the end of the path, and pointers to the incoming and outgoing +/// arcs used to construct the path. Labels support dominance checking and forward +/// extension along an arc. +/// +/// @tparam ResourceType The resource type used to track consumption along the path. +/// Must satisfy @c ResourceTypeConcept. template requires ResourceTypeConcept class Label { friend class LabelFactory; public: - // Label ID + /// @brief Unique identifier for this label within its factory's allocation pool. size_t id; + /// @brief Constructs a label with only an id and resource; no graph position. + /// + /// @param label_id Numeric identifier assigned by the owning @c LabelFactory. + /// @param resource Owning pointer to the resource state for this label. Label(size_t label_id, std::unique_ptr> resource) : id(label_id), dominated(false), @@ -35,6 +48,15 @@ class Label { in_arc_(nullptr), out_arc_(nullptr) {} + /// @brief Constructs a label with full graph-position information. + /// + /// @param label_id Numeric identifier assigned by the owning @c LabelFactory. + /// @param resource Owning pointer to the resource state for this label. + /// @param end_node Pointer to the node at the end of the partial path. + /// @param in_arc Pointer to the arc via which this label was extended forward + /// (may be @c nullptr for the source label). + /// @param out_arc Pointer to the arc via which this label was extended backward + /// (may be @c nullptr for forward labels). Label(size_t label_id, std::unique_ptr> resource, const Node* end_node, const Arc* in_arc, const Arc* out_arc) @@ -45,12 +67,26 @@ class Label { in_arc_(in_arc), out_arc_(out_arc) {} - // Check dominance + /// @brief Tests whether this label dominates @p rhs_label. + /// + /// A label @c a dominates label @c b when every resource consumed by @c a is + /// no greater than the corresponding resource consumed by @c b (i.e., @c a is + /// at least as good as @c b in every dimension). + /// + /// @param rhs_label The label to compare against. + /// @return @c true if @c *this dominates @p rhs_label. [[nodiscard]] bool operator<=(const Label& rhs_label) const { return *resource_ <= *rhs_label.resource_; } - // Label extension + /// @brief Extends this label along @p arc and writes the result into @p extended_label. + /// + /// The arc's extender is invoked to propagate the resource state, and the + /// graph-position fields of @p extended_label are updated accordingly. + /// + /// @param arc The arc along which to extend. + /// @param extended_label Output label that will hold the extended state. + /// Must be a valid, pre-allocated @c Label object. void extend(const Arc& arc, Label* extended_label) const { arc.extender->extend(*resource_, extended_label->resource_.get()); extended_label->end_node_ = arc.destination; @@ -58,38 +94,65 @@ class Label { extended_label->out_arc_ = nullptr; } - // Return label cost + /// @brief Returns the accumulated cost of the partial path represented by this label. + /// + /// @return The cost value stored in the underlying resource. [[nodiscard]] double get_cost() const { return resource_->get_cost(); } - // Return true if the label is feasible + /// @brief Returns whether this label's resource state satisfies all feasibility + /// constraints. + /// + /// @return @c true if the label is feasible. [[nodiscard]] bool is_feasible() const { return resource_->is_feasible(); } - // Return true if the label can reach the given node + /// @brief Returns whether this label can still reach the specified destination node. + /// + /// @param destination_node_id The ID of the node to test reachability for. + /// @return @c true if the destination node is reachable from the current state. [[nodiscard]] bool is_reachable(size_t destination_node_id) const { return resource_->is_reachable(destination_node_id); } + /// @brief Returns a reference to the resource state tracked by this label. + /// + /// @return Reference to the underlying @c Resource object. [[nodiscard]] Resource& get_resource() const { return *resource_; } + /// @brief Returns a pointer to the node at the end of this label's partial path. + /// + /// @return Pointer to the end node, or @c nullptr if not yet assigned. [[nodiscard]] const Node* get_end_node() const { return end_node_; } + /// @brief Returns a pointer to the arc used for the most recent forward extension. + /// + /// @return Pointer to the incoming arc, or @c nullptr for the source label. [[nodiscard]] const Arc* get_in_arc() const { return in_arc_; } + /// @brief Sets the predecessor label and increments its reference count. + /// + /// Establishes the backward path linkage from this label to @p predecessor. + /// The predecessor's @c ref_count is incremented to prevent premature release. + /// + /// @param predecessor Pointer to the predecessor label in the path. void set_prev_label(Label* predecessor) { prev_label = predecessor; ++predecessor->ref_count; } + /// @brief Flag indicating that this label has been dominated and can be discarded. bool dominated; - // Predecessor label set at extension time; valid as long as ref_count keeps it pinned. + /// @brief Predecessor label in the path; valid as long as @c ref_count keeps it pinned. Label* prev_label = nullptr; - // Number of alive successors that reference this label as their predecessor. 32 bits so a - // high-out-degree node (e.g. a dense VRP pricing graph that extends one label to many - // hundreds or thousands of successors) cannot overflow the count, which would make - // release_with_ref_count() free a still-referenced predecessor. + + /// @brief Number of live successor labels that reference this label as their predecessor. + /// + /// Using 32 bits to accommodate high-out-degree nodes (e.g., dense VRP pricing graphs) + /// without overflow, which would cause @c release_with_ref_count() to free a + /// still-referenced predecessor. uint32_t ref_count = 0; - // True when the algorithm wanted to release this label but ref_count was > 0. + + /// @brief True when the algorithm wanted to release this label but @c ref_count was > 0. bool pending_release = false; private: diff --git a/cpp/rcspp/label/label_factory.hpp b/cpp/rcspp/label/label_factory.hpp index 92375e21..bab59db9 100644 --- a/cpp/rcspp/label/label_factory.hpp +++ b/cpp/rcspp/label/label_factory.hpp @@ -11,12 +11,35 @@ namespace rcspp { +/// @brief Factory for creating and resetting @c Label objects. +/// +/// @c LabelFactory owns a reference to a @c ResourceFactory and uses it to +/// allocate fresh resource states when constructing new labels. It also provides +/// a static helper to reinitialise an existing label in-place, enabling label +/// recycling without heap allocation. +/// +/// @tparam ResourceType The resource type used by the labels produced by this factory. template class LabelFactory { public: + /// @brief Constructs a @c LabelFactory backed by the given @c ResourceFactory. + /// + /// @param resource_factory Pointer to the resource factory used to allocate + /// resource states for new labels. Must outlive this factory. explicit LabelFactory(ResourceFactory* resource_factory) : resource_factory_(*resource_factory) {} + /// @brief Allocates and initialises a new label at the specified graph position. + /// + /// A fresh resource state is copied from the end node and wrapped in the new label. + /// + /// @param label_id Numeric identifier to assign to the new label. + /// @param end_node Pointer to the node at the end of the partial path. + /// @param in_arc Optional pointer to the arc used for the forward extension + /// that produced this label (defaults to @c nullptr). + /// @param out_arc Optional pointer to the arc used for the backward extension + /// that produced this label (defaults to @c nullptr). + /// @return An owning @c unique_ptr to the newly constructed label. std::unique_ptr> make_label( size_t label_id, const Node* end_node, const Arc* in_arc = nullptr, const Arc* out_arc = nullptr) { @@ -29,6 +52,17 @@ class LabelFactory { out_arc); } + /// @brief Resets an existing label to a fresh state at the specified graph position. + /// + /// All bookkeeping fields (@c dominated, @c prev_label, @c ref_count, + /// @c pending_release) are cleared, and the label's resource is reset to the + /// initial state of @p end_node. This enables label recycling without allocation. + /// + /// @param label Pointer to the label to reset. Must not be @c nullptr. + /// @param label_id New numeric identifier to assign to the label. + /// @param end_node Pointer to the node at the end of the new partial path. + /// @param in_arc Optional pointer to the incoming arc (defaults to @c nullptr). + /// @param out_arc Optional pointer to the outgoing arc (defaults to @c nullptr). static void reset_label(Label* label, size_t label_id, const Node* end_node, const Arc* in_arc = nullptr, diff --git a/cpp/rcspp/preprocessor/feasibility_preprocessor.hpp b/cpp/rcspp/preprocessor/feasibility_preprocessor.hpp index baa49fef..25d08c8f 100644 --- a/cpp/rcspp/preprocessor/feasibility_preprocessor.hpp +++ b/cpp/rcspp/preprocessor/feasibility_preprocessor.hpp @@ -12,9 +12,24 @@ namespace rcspp { +/// @brief Preprocessor that removes arcs along which no feasible resource extension exists. +/// +/// For every node, `FeasibilityPreprocessor` computes a set of initial resource states +/// reachable from any source node. An arc is then removed if none of those initial states +/// can be extended through the arc to produce a feasible resource at the destination node. +/// +/// @tparam ResourceType The resource type used in the graph. template class FeasibilityPreprocessor final : public Preprocessor { public: + /// @brief Constructs the preprocessor and pre-computes per-node initial resources. + /// + /// Source nodes receive the default resource provided by the factory. For all + /// other nodes, feasible initial resources are obtained by extending the default + /// resource of each predecessor through its incoming arc. + /// + /// @param resource_factory Factory used to create and copy resource objects. + /// @param graph Non-owning pointer to the graph to preprocess. FeasibilityPreprocessor(ResourceFactory* resource_factory, Graph* graph) : Preprocessor(graph), resource_factory_(resource_factory) { diff --git a/cpp/rcspp/preprocessor/preprocessor.hpp b/cpp/rcspp/preprocessor/preprocessor.hpp index eb534174..dfa63d05 100644 --- a/cpp/rcspp/preprocessor/preprocessor.hpp +++ b/cpp/rcspp/preprocessor/preprocessor.hpp @@ -9,13 +9,33 @@ namespace rcspp { +/// @brief Base class for graph preprocessors that remove infeasible or dominated arcs. +/// +/// A `Preprocessor` operates on a `Graph` and iteratively removes arcs that are deemed +/// unnecessary by the concrete subclass's `remove_arc` predicate. Removed arcs can be +/// restored to their original state via `restore()`. +/// +/// @tparam ResourceType The resource type used in the graph; must satisfy +/// `ResourceTypeConcept`. template requires ResourceTypeConcept class Preprocessor { public: + /// @brief Constructs a preprocessor attached to the given graph. + /// + /// @param graph Non-owning pointer to the graph to preprocess. explicit Preprocessor(Graph* graph) : graph_(graph) {} + + /// @brief Virtual destructor. virtual ~Preprocessor() = default; + /// @brief Removes arcs that are identified as unnecessary by `remove_arc`. + /// + /// Iterates over all arcs and removes any arc for which `remove_arc` returns + /// `true`. If preprocessing is disabled (`disable_preprocessing_` is set), the + /// method returns immediately without modifying the graph. + /// + /// @return `true` if at least one arc was removed, `false` otherwise. virtual bool preprocess() { if (disable_preprocessing_) { return false; @@ -26,6 +46,10 @@ class Preprocessor { return !arc_ids.empty(); } + /// @brief Restores all arcs that were removed by previous calls to `preprocess`. + /// + /// After this call the graph is in the same state it was before any preprocessing + /// was applied. virtual void restore() { for (const auto& arc_id : removed_arcs_by_id_) { graph_->restore_arc(arc_id); @@ -38,7 +62,19 @@ class Preprocessor { std::vector removed_arcs_by_id_; protected: + /// @brief When set to `true`, `preprocess()` becomes a no-op. + /// + /// Subclasses should set this flag when preprocessing cannot be safely performed + /// (e.g., when a required bound is infinite or a negative cycle is detected). bool disable_preprocessing_ = false; + + /// @brief Determines whether a given arc should be removed from the graph. + /// + /// Subclasses override this method to implement their specific removal criterion. + /// The default implementation never removes any arc. + /// + /// @param arc The arc to evaluate. + /// @return `true` if the arc should be removed, `false` otherwise. virtual bool remove_arc(const Arc& arc) { return false; } }; } // namespace rcspp diff --git a/cpp/rcspp/preprocessor/shortest_path_connectivity_sort.hpp b/cpp/rcspp/preprocessor/shortest_path_connectivity_sort.hpp index d1dfa07a..84c9761d 100644 --- a/cpp/rcspp/preprocessor/shortest_path_connectivity_sort.hpp +++ b/cpp/rcspp/preprocessor/shortest_path_connectivity_sort.hpp @@ -12,17 +12,54 @@ #include "rcspp/resource/concrete/numerical_resource.hpp" namespace rcspp { + +/// @brief Sorts graph nodes using shortest-path distances and connectivity heuristics. +/// +/// Reorders the nodes of a `Graph` in place to improve the efficiency of subsequent +/// label-setting algorithms. The ordering criterion is applied in priority order: +/// +/// 1. Source nodes first, sink nodes last. +/// 2. Connectivity asymmetry: if `node1` can reach `node2` but not vice versa, +/// `node1` is placed earlier. +/// 3. Fewer reachable successors first (more constrained nodes are expanded earlier). +/// 4. Fewer reverse-reachable predecessors first. +/// 5. Closer to sources (ascending distance from sources), then farther from sinks +/// (descending distance to sinks) when Bellman-Ford distances are available. +/// 6. Fewer direct arcs from `node1` to `node2`. +/// 7. Tie-break by node id. +/// +/// @tparam CostResourceType Numerical resource type used to compute shortest-path +/// distances; must satisfy `is_numerical_resource_v`. Defaults to +/// `RealResource`. +/// @tparam ResourceTypes Remaining resource types that form the composition. template requires is_numerical_resource_v class ShortestPathConnectivitySort { private: + /// @brief Hash functor for `std::pair` arc keys. struct DirectArcKeyHash { + /// @brief Computes a hash value for a directed arc identified by its + /// origin and destination node ids. + /// + /// @param key Pair of (origin_id, destination_id). + /// @return Combined hash value. size_t operator()(const std::pair& key) const noexcept { return std::hash{}(key.first) ^ (std::hash{}(key.second) << 1); } }; public: + /// @brief Constructs the sorter and immediately reorders the graph's nodes. + /// + /// Bellman-Ford is run from sources and to sinks to obtain distance maps. If a + /// negative cycle is detected, distance-based tie-breaking is skipped. The + /// connectivity matrix is used for reachability heuristics. + /// + /// @param graph Non-owning pointer to the graph whose nodes will be sorted. + /// @param cm Non-owning pointer to the precomputed connectivity matrix. + /// @param cost_index Index of the cost component within the resource composition + /// to use for shortest-path distances. Pass `std::nullopt` to + /// use the default cost component. explicit ShortestPathConnectivitySort( // NOLINT Graph>* graph, ConnectivityMatrix>* cm, diff --git a/cpp/rcspp/preprocessor/shortest_path_preprocessor.hpp b/cpp/rcspp/preprocessor/shortest_path_preprocessor.hpp index e64123b8..37327327 100644 --- a/cpp/rcspp/preprocessor/shortest_path_preprocessor.hpp +++ b/cpp/rcspp/preprocessor/shortest_path_preprocessor.hpp @@ -16,11 +16,34 @@ namespace rcspp { +/// @brief Preprocessor that removes arcs whose cost cannot be part of any optimal path. +/// +/// Uses Bellman-Ford shortest-path distances from sources and to sinks to prune arcs: +/// an arc `(u, v)` with cost `c` is removed when +/// `dist_from_source[u] + c + dist_to_sink[v] > upper_bound`. +/// +/// Preprocessing is automatically disabled when `upper_bound` is infinite or when +/// Bellman-Ford detects a negative-cost cycle. +/// +/// @tparam CostResourceType Numerical resource type used to measure arc cost; must +/// satisfy `is_numerical_resource_v`. Defaults to `RealResource`. +/// @tparam ResourceTypes Remaining resource types that form the composition. template requires is_numerical_resource_v class ShortestPathPreprocessor final : public Preprocessor> { public: + /// @brief Constructs the preprocessor and runs Bellman-Ford in both directions. + /// + /// If `upper_bound` is infinite, preprocessing is disabled. If Bellman-Ford + /// detects a negative cycle, preprocessing is also disabled. + /// + /// @param graph Non-owning pointer to the graph to preprocess. + /// @param upper_bound Known upper bound on the total path cost. Arcs that + /// cannot belong to a path with cost at most this value are + /// removed. + /// @param cost_index Index of the cost component within the resource + /// composition. Defaults to `0`. ShortestPathPreprocessor(Graph>* graph, double upper_bound, size_t cost_index = 0) : Preprocessor>(graph), diff --git a/cpp/rcspp/rcspp.hpp b/cpp/rcspp/rcspp.hpp index 08cab998..8654dd18 100644 --- a/cpp/rcspp/rcspp.hpp +++ b/cpp/rcspp/rcspp.hpp @@ -1,4 +1,18 @@ // Automatically generated umbrella header clang-format off NOLINT(legal/copyright) + +/// @file rcspp.hpp +/// @brief Umbrella header for the RCSPP library. +/// +/// Including this single header pulls in every public component of the RCSPP +/// (Resource-Constrained Shortest Path Problem) library: graph primitives, label +/// management, resource definitions, preprocessing utilities, search algorithms, +/// and general helpers. +/// +/// Typical usage: +/// @code +/// #include "rcspp/rcspp.hpp" +/// @endcode + #pragma once #include "rcspp/algorithm/algorithm.hpp" diff --git a/cpp/rcspp/resource/base/extender.hpp b/cpp/rcspp/resource/base/extender.hpp index 62841c0e..d2cb405a 100644 --- a/cpp/rcspp/resource/base/extender.hpp +++ b/cpp/rcspp/resource/base/extender.hpp @@ -12,28 +12,59 @@ namespace rcspp { -// Definition of ExtenderPrototype for Extender +/// @brief Concrete arc extender that applies a typed extension function to a resource label. +/// +/// `Extender` is the leaf of the `ExtenderPrototype` CRTP hierarchy. One `Extender` +/// instance lives on each arc of the resource graph for each resource dimension. When +/// the solver extends a label along an arc, it calls `extend` (forward direction) or +/// `extend_back` (backward direction) on every extender associated with that arc. +/// +/// @tparam ResourceType The resource value type; must satisfy `ResourceTypeConcept`. template requires ResourceTypeConcept class Extender : public ExtenderPrototype, ResourceType> { using Prototype = ExtenderPrototype; public: + /// @brief Constructs an extender with a copied resource value and extension function. + /// + /// @param resource_value Arc resource value (copied). + /// @param extension_function Owned extension function. + /// @param arc_id Identifier of the associated arc. Extender(const ResourceType& resource_value, std::unique_ptr> extension_function, const size_t arc_id) : Prototype(resource_value, std::move(extension_function), arc_id) {} + /// @brief Constructs an extender by unpacking a tuple into the resource-value constructor. + /// + /// @tparam Args Argument types packed in the initialiser tuple. + /// @param resource_initializer Tuple whose elements initialise the arc `ResourceType`. + /// @param extension_function Owned extension function. + /// @param arc_id Identifier of the associated arc. template Extender(const std::tuple& resource_initializer, std::unique_ptr> extension_function, const size_t arc_id) : Prototype(resource_initializer, std::move(extension_function), arc_id) {} + /// @brief Constructs a default-value extender with the given extension function. + /// + /// @param extension_function Owned extension function. + /// @param arc_id Identifier of the associated arc. Extender(std::unique_ptr> extension_function, const size_t arc_id) : Prototype(std::move(extension_function), arc_id) {} + /// @brief Creates a new extender cloned for a specific arc of a (possibly different) + /// resource graph. + /// + /// The extension function is re-created for the new arc via + /// `ExtensionFunction::create(arc)`, so any arc-specific state is refreshed. + /// + /// @tparam GraphResourceType Resource type of the target graph arc. + /// @param arc Target arc for which the clone is created. + /// @return A heap-allocated `Extender` bound to `arc`. template [[nodiscard]] auto clone(const Arc& arc) const -> std::unique_ptr { @@ -43,6 +74,14 @@ class Extender : public ExtenderPrototype, ResourceType> } // Resource extension + /// @brief Extends a label in the forward direction along the arc. + /// + /// Delegates to the stored `ExtensionFunction::extend`, passing the current + /// resource value and the arc's resource value, and writing the result into + /// `extended_resource`. + /// + /// @param resource The label resource before extension. + /// @param extended_resource Output resource that receives the extended value. void extend(const Resource& resource, Resource* extended_resource) const { this->extension_function_->extend(resource.get_value(), @@ -50,6 +89,12 @@ class Extender : public ExtenderPrototype, ResourceType> &extended_resource->get_value()); } + /// @brief Extends a label in the backward direction along the arc. + /// + /// Delegates to `ExtensionFunction::extend_back` for backward labelling. + /// + /// @param resource The backward label resource before extension. + /// @param extended_resource Output resource that receives the extended value. void extend_back(const Resource& resource, Resource* extended_resource) const { this->extension_function_->extend_back(resource.get_value(), @@ -57,6 +102,9 @@ class Extender : public ExtenderPrototype, ResourceType> &extended_resource->get_value()); } + /// @brief Returns a human-readable string representation of the arc resource value. + /// + /// @return String representation of the stored resource value. [[nodiscard]] std::string to_string() const { return this->value_.to_string(); } }; } // namespace rcspp diff --git a/cpp/rcspp/resource/base/extender_prototype.hpp b/cpp/rcspp/resource/base/extender_prototype.hpp index 4e4c054a..e773dcb9 100644 --- a/cpp/rcspp/resource/base/extender_prototype.hpp +++ b/cpp/rcspp/resource/base/extender_prototype.hpp @@ -16,12 +16,30 @@ namespace rcspp { +/// @brief CRTP base class for arc extender objects used in label extension. +/// +/// An `ExtenderPrototype` stores the arc-local resource value (e.g. consumption on that +/// arc) together with an `ExtensionFunction` that knows how to propagate a label's +/// resource along the arc. The CRTP pattern allows the base to return `unique_ptr` to +/// the concrete derived type from `clone()` without virtual dispatch. +/// +/// @tparam ExtenderClass The concrete derived class (CRTP parameter). +/// @tparam ResourceType The resource value type; must satisfy `ResourceTypeConcept`. template requires ResourceTypeConcept class ExtenderPrototype { public: + /// @brief Default constructor. + /// + /// Initialises the resource value to its default, sets the extension function to + /// `nullptr`, and the arc identifier to 0. ExtenderPrototype() : value_(), extension_function_(nullptr), arc_id_(0) {} + /// @brief Constructs an extender with an explicit resource value and extension function. + /// + /// @param resource_value Arc resource value (moved into the extender). + /// @param extension_function Owned extension function applied during label propagation. + /// @param arc_id Identifier of the arc this extender is associated with. ExtenderPrototype(ResourceType resource_value, std::unique_ptr> extension_function, const size_t arc_id) @@ -29,6 +47,14 @@ class ExtenderPrototype { extension_function_(std::move(extension_function)), arc_id_(arc_id) {} + /// @brief Constructs an extender by unpacking a tuple into the resource-value constructor. + /// + /// The tuple elements are forwarded as individual constructor arguments to `ResourceType`. + /// + /// @tparam Args Argument types packed in the tuple. + /// @param resource_initializer Tuple whose elements initialise the `ResourceType`. + /// @param extension_function Owned extension function. + /// @param arc_id Identifier of the associated arc. template ExtenderPrototype(const std::tuple& resource_initializer, std::unique_ptr> extension_function, @@ -41,23 +67,46 @@ class ExtenderPrototype { extension_function_(std::move(extension_function)), arc_id_(arc_id) {} + /// @brief Constructs a default-value extender with the given extension function. + /// + /// @param extension_function Owned extension function. + /// @param arc_id Identifier of the associated arc. ExtenderPrototype(std::unique_ptr> extension_function, const size_t arc_id) : value_(), extension_function_(std::move(extension_function)), arc_id_(arc_id) {} + /// @brief Creates a deep copy of this extender. + /// + /// @return A heap-allocated clone of the concrete derived extender. [[nodiscard]] auto clone() const -> std::unique_ptr { return std::make_unique(downcast()); } + /// @brief Returns a const reference to the stored arc resource value. + /// + /// @return Const reference to the resource value. [[nodiscard]] auto get_value() const -> const ResourceType& { return value_; } + + /// @brief Returns a mutable reference to the stored arc resource value. + /// + /// @return Mutable reference to the resource value. [[nodiscard]] auto get_value() -> ResourceType& { return value_; } // Forward set_value calls to the stored value (only valid when ResourceType has set_value) + /// @brief Forwards a value-setting call to the underlying arc resource-value object. + /// + /// Only valid when `ResourceType` itself exposes a `set_value` method. + /// + /// @tparam Args Argument types forwarded to `ResourceType::set_value`. + /// @param args Arguments forwarded to `ResourceType::set_value`. template void set_value(Args&&... args) { value_.set_value(std::forward(args)...); } + /// @brief Returns the identifier of the arc associated with this extender. + /// + /// @return Arc identifier. [[nodiscard]] auto get_arc_id() const -> size_t { return arc_id_; } protected: diff --git a/cpp/rcspp/resource/base/resource.hpp b/cpp/rcspp/resource/base/resource.hpp index dc5a1a06..62769460 100644 --- a/cpp/rcspp/resource/base/resource.hpp +++ b/cpp/rcspp/resource/base/resource.hpp @@ -11,15 +11,34 @@ namespace rcspp { -// Definition of ResourcePrototype for Resource +/// @brief Concrete resource type used during label extension in the RCSPP algorithm. +/// +/// `Resource` is the primary building block of a label: it holds a typed resource value +/// together with the dominance, feasibility, and cost functions required to evaluate that +/// label at a given graph node. +/// +/// This class is the leaf of the `ResourcePrototype` CRTP hierarchy and adds the +/// domain-level query operations (`operator<=`, `is_lower`, `get_cost`, `is_feasible`, +/// etc.) that are called by the solver's inner loop. +/// +/// @tparam ResourceType The value type stored inside this resource; must satisfy +/// `ResourceTypeConcept`. template requires ResourceTypeConcept class Resource : public ResourcePrototype, ResourceType> { using Prototype = ResourcePrototype; public: + /// @brief Default constructor. Resource() = default; + /// @brief Constructs a resource with a copied value and exclusively-owned function objects. + /// + /// @param resource_value Initial resource value (copied). + /// @param dominance_function Owned dominance function. + /// @param feasibility_function Owned feasibility function. + /// @param cost_function Owned cost function. + /// @param node_id Associated graph node (default 0). Resource(const ResourceType& resource_value, std::unique_ptr> dominance_function, std::unique_ptr> feasibility_function, @@ -27,12 +46,26 @@ class Resource : public ResourcePrototype, ResourceType> : Prototype(resource_value, std::move(dominance_function), std::move(feasibility_function), std::move(cost_function), node_id) {} + /// @brief Constructs a default-value resource with exclusively-owned function objects. + /// + /// @param dominance_function Owned dominance function. + /// @param feasibility_function Owned feasibility function. + /// @param cost_function Owned cost function. + /// @param node_id Associated graph node (default 0). Resource(std::unique_ptr> dominance_function, std::unique_ptr> feasibility_function, std::unique_ptr> cost_function, std::size_t node_id = 0) : Prototype(std::move(dominance_function), std::move(feasibility_function), std::move(cost_function), node_id) {} + /// @brief Constructs a resource with a copied value and borrowed (non-owning) function + /// objects. + /// + /// @param resource_value Initial resource value (copied). + /// @param dominance_function Non-owning pointer to the dominance function. + /// @param feasibility_function Non-owning pointer to the feasibility function. + /// @param cost_function Non-owning pointer to the cost function. + /// @param node_id Associated graph node (default 0). Resource(const ResourceType& resource_value, DominanceFunction* dominance_function, FeasibilityFunction* feasibility_function, @@ -40,26 +73,58 @@ class Resource : public ResourcePrototype, ResourceType> : Prototype(resource_value, std::move(dominance_function), std::move(feasibility_function), std::move(cost_function), node_id) {} + /// @brief Constructs a default-value resource with borrowed (non-owning) function objects. + /// + /// @param dominance_function Non-owning pointer to the dominance function. + /// @param feasibility_function Non-owning pointer to the feasibility function. + /// @param cost_function Non-owning pointer to the cost function. + /// @param node_id Associated graph node (default 0). Resource(DominanceFunction* dominance_function, FeasibilityFunction* feasibility_function, CostFunction* cost_function, std::size_t node_id = 0) : Prototype(std::move(dominance_function), std::move(feasibility_function), std::move(cost_function), node_id) {} + /// @brief Copy constructor. + /// + /// @param rhs_resource Resource to copy. Resource(Resource const& rhs_resource) : Prototype(rhs_resource) {} + /// @brief Move constructor. + /// + /// @param rhs_resource Resource to move from. Resource(Resource&& rhs_resource) noexcept : Prototype(std::move(rhs_resource)) {} + /// @brief Swaps two `Resource` objects without throwing. + /// + /// @param first First resource. + /// @param second Second resource. static void swap(Resource& first, Resource& second) noexcept { ResourcePrototype::swap(first, second); } // Check dominance — passes value_ for simple types, full Resource for composition types + /// @brief Dominance check: returns `true` if this resource dominates `rhs_resource`. + /// + /// A resource `a` dominates `b` (`a <= b`) when `a` is at least as good as `b` on all + /// dimensions according to the configured `DominanceFunction`. + /// + /// @param rhs_resource The resource to compare against. + /// @return `true` if this resource dominates `rhs_resource`. auto operator<=(const Resource& rhs_resource) const -> bool { return this->dominance_function_->check_dominance(this->value_, rhs_resource.value_); } // Check distance from the resource to another + /// @brief Fast dominance check with a relaxation delta. + /// + /// Uses the dominance function's `fast_check_dominance` method, which may apply an + /// additive tolerance `delta` to speed up dominance screening. + /// + /// @param rhs_resource The resource to compare against. + /// @param delta Relaxation tolerance (default 0). + /// @return `true` if this resource is considered lower than (dominated by) `rhs_resource` + /// within the given tolerance. [[nodiscard]] auto is_lower(const Resource& rhs_resource, double delta = 0) const -> bool { return this->dominance_function_->fast_check_dominance(this->value_, rhs_resource.value_, @@ -67,23 +132,48 @@ class Resource : public ResourcePrototype, ResourceType> } // Return resource cost + /// @brief Returns the scalar cost associated with this resource's current value. + /// + /// @return Cost as computed by the configured `CostFunction`. [[nodiscard]] auto get_cost() const -> double { return this->cost_function_->get_cost(this->value_); } // Return true if the resource is feasible + /// @brief Returns `true` if this resource satisfies all forward-direction feasibility + /// constraints. + /// + /// @return `true` when the resource is feasible in the forward direction. [[nodiscard]] auto is_feasible() const -> bool { return this->feasibility_function_->is_feasible(this->value_); } + /// @brief Returns `true` if this resource satisfies all backward-direction feasibility + /// constraints. + /// + /// @return `true` when the resource is feasible in the backward direction. [[nodiscard]] auto is_back_feasible() const -> bool { return this->feasibility_function_->is_back_feasible(this->value_); } + /// @brief Returns `true` if this (forward) resource can be merged with a backward label. + /// + /// Used in bidirectional labelling to determine whether a forward and a backward label + /// can be joined into a complete path. + /// + /// @param back_resource The backward resource to attempt merging with. + /// @return `true` when the two labels are compatible for merging. [[nodiscard]] auto can_be_merged(const Resource& back_resource) const -> bool { return this->feasibility_function_->can_be_merged(this->value_, back_resource.value_); } + /// @brief Returns `true` if the destination node is reachable from this resource's state. + /// + /// Delegates to the feasibility function's reachability check, which may use ng-route + /// or other neighbourhood information. + /// + /// @param destination_node_id Identifier of the node whose reachability is queried. + /// @return `true` when the destination node can still be reached. [[nodiscard]] auto is_reachable(size_t destination_node_id) const -> bool { return this->feasibility_function_->is_reachable(*this, destination_node_id); } diff --git a/cpp/rcspp/resource/base/resource_prototype.hpp b/cpp/rcspp/resource/base/resource_prototype.hpp index 8181049c..28aa1fd2 100644 --- a/cpp/rcspp/resource/base/resource_prototype.hpp +++ b/cpp/rcspp/resource/base/resource_prototype.hpp @@ -18,10 +18,28 @@ namespace rcspp { +/// @brief CRTP base class that provides common state and behaviour for resource objects. +/// +/// `ResourcePrototype` stores the resource value together with its associated dominance, +/// feasibility, and cost function objects. It implements the copy-and-swap assignment +/// idiom and offers factory methods (`create`, `copy`, `clone`) that derived classes +/// inherit without duplication. +/// +/// Ownership of the three function objects can be either *exclusive* (via `unique_ptr` +/// members) or *shared/borrowed* (via raw-pointer members). Both ownership modes are +/// supported by separate constructor overloads so that labels can cheaply share +/// per-node function objects created once by the graph builder. +/// +/// @tparam ResourceClass The concrete derived class (CRTP parameter). +/// @tparam ResourceType The value type stored inside the resource; must satisfy +/// `ResourceTypeConcept`. template requires ResourceTypeConcept class ResourcePrototype { public: + /// @brief Default constructor. + /// + /// Constructs a resource with a default-initialised value and null function pointers. ResourcePrototype() : value_(), unique_dominance_function_(nullptr), @@ -32,6 +50,13 @@ class ResourcePrototype { cost_function_(nullptr), node_id_(0) {} + /// @brief Constructs a resource with a copied value and exclusively-owned function objects. + /// + /// @param resource_value Initial resource value (copied). + /// @param dominance_function Owned dominance function for this resource. + /// @param feasibility_function Owned feasibility function for this resource. + /// @param cost_function Owned cost function for this resource. + /// @param node_id Graph node associated with this resource (default 0). ResourcePrototype(const ResourceType& resource_value, std::unique_ptr> dominance_function, std::unique_ptr> feasibility_function, @@ -46,6 +71,13 @@ class ResourcePrototype { cost_function_(unique_cost_function_.get()), node_id_(node_id) {} + /// @brief Constructs a resource with a moved value and exclusively-owned function objects. + /// + /// @param resource_value Initial resource value (moved). + /// @param dominance_function Owned dominance function for this resource. + /// @param feasibility_function Owned feasibility function for this resource. + /// @param cost_function Owned cost function for this resource. + /// @param node_id Graph node associated with this resource (default 0). ResourcePrototype(ResourceType&& resource_value, std::unique_ptr> dominance_function, std::unique_ptr> feasibility_function, @@ -60,6 +92,12 @@ class ResourcePrototype { cost_function_(unique_cost_function_.get()), node_id_(node_id) {} + /// @brief Constructs a default-value resource with exclusively-owned function objects. + /// + /// @param dominance_function Owned dominance function for this resource. + /// @param feasibility_function Owned feasibility function for this resource. + /// @param cost_function Owned cost function for this resource. + /// @param node_id Graph node associated with this resource (default 0). ResourcePrototype(std::unique_ptr> dominance_function, std::unique_ptr> feasibility_function, std::unique_ptr> cost_function, @@ -73,6 +111,17 @@ class ResourcePrototype { cost_function_(unique_cost_function_.get()), node_id_(node_id) {} + /// @brief Constructs a resource with a copied value and borrowed (non-owning) function + /// objects. + /// + /// The caller is responsible for keeping the pointed-to function objects alive for the + /// lifetime of this resource. + /// + /// @param resource_value Initial resource value (copied). + /// @param dominance_function Non-owning pointer to the dominance function. + /// @param feasibility_function Non-owning pointer to the feasibility function. + /// @param cost_function Non-owning pointer to the cost function. + /// @param node_id Graph node associated with this resource (default 0). ResourcePrototype(const ResourceType& resource_value, DominanceFunction* dominance_function, FeasibilityFunction* feasibility_function, @@ -83,6 +132,14 @@ class ResourcePrototype { cost_function_(cost_function), node_id_(node_id) {} + /// @brief Constructs a resource with a moved value and borrowed (non-owning) function + /// objects. + /// + /// @param resource_value Initial resource value (moved). + /// @param dominance_function Non-owning pointer to the dominance function. + /// @param feasibility_function Non-owning pointer to the feasibility function. + /// @param cost_function Non-owning pointer to the cost function. + /// @param node_id Graph node associated with this resource (default 0). ResourcePrototype(ResourceType&& resource_value, DominanceFunction* dominance_function, FeasibilityFunction* feasibility_function, @@ -93,6 +150,12 @@ class ResourcePrototype { cost_function_(cost_function), node_id_(node_id) {} + /// @brief Constructs a default-value resource with borrowed (non-owning) function objects. + /// + /// @param dominance_function Non-owning pointer to the dominance function. + /// @param feasibility_function Non-owning pointer to the feasibility function. + /// @param cost_function Non-owning pointer to the cost function. + /// @param node_id Graph node associated with this resource (default 0). ResourcePrototype(DominanceFunction* dominance_function, FeasibilityFunction* feasibility_function, CostFunction* cost_function, std::size_t node_id = 0) @@ -102,6 +165,12 @@ class ResourcePrototype { cost_function_(cost_function), node_id_(node_id) {} + /// @brief Copy constructor — deep-copies the value and clones owned function objects. + /// + /// If a function object is exclusively owned (via `unique_ptr`), it is cloned; otherwise + /// the raw pointer is copied (borrowed ownership is preserved). + /// + /// @param rhs_resource Source resource to copy from. explicit ResourcePrototype(ResourceClass const& rhs_resource) : value_(rhs_resource.value_), unique_dominance_function_(rhs_resource.unique_dominance_function_ @@ -122,18 +191,31 @@ class ResourcePrototype { : rhs_resource.cost_function_), node_id_(rhs_resource.get_node_id()) {} + /// @brief Move constructor — transfers ownership via copy-and-swap. + /// + /// @param rhs_resource Source resource to move from. explicit ResourcePrototype(ResourceClass&& rhs_resource) : ResourcePrototype() { swap(*this, rhs_resource); } ~ResourcePrototype() = default; + /// @brief Copy-and-swap assignment operator. + /// + /// @param rhs_resource Resource to assign (passed by value to elide an extra copy). + /// @return Reference to `*this` as the derived `ResourceClass`. auto operator=(ResourceClass rhs_resource) -> ResourceClass& { swap(*this, rhs_resource); return downcast(); } - // To implement the copy-and-swap idiom + /// @brief Swaps all members of two `ResourcePrototype` objects without throwing. + /// + /// Required by the copy-and-swap assignment idiom. Swaps the stored value, all + /// owned and borrowed function-object pointers, and the node identifier. + /// + /// @param first First resource to swap. + /// @param second Second resource to swap. friend void swap(ResourcePrototype& first, ResourcePrototype& second) noexcept { using std::swap; @@ -151,24 +233,49 @@ class ResourcePrototype { swap(first.node_id_, second.node_id_); } + /// @brief Creates a deep copy of this resource, including cloned function objects. + /// + /// @return A heap-allocated clone of the concrete derived object. [[nodiscard]] auto clone() const -> std::unique_ptr { return std::make_unique(downcast()); } + /// @brief Returns the graph node identifier associated with this resource. + /// + /// @return Node identifier. [[nodiscard]] auto get_node_id() const -> size_t { return node_id_; } // Read-only access to the stored resource value + /// @brief Returns a const reference to the stored resource value. + /// + /// @return Const reference to the resource value. [[nodiscard]] auto get_value() const -> const ResourceType& { return value_; } // Mutable access — used internally (e.g. by ExtenderPrototype) to pass ResourceType* + /// @brief Returns a mutable reference to the stored resource value. + /// + /// @return Mutable reference to the resource value. [[nodiscard]] auto get_value() -> ResourceType& { return value_; } // Forward set_value calls to the stored value (only valid when ResourceType has set_value) + /// @brief Forwards a value-setting call to the underlying resource-value object. + /// + /// Only valid when `ResourceType` itself exposes a `set_value` method. + /// + /// @tparam Args Argument types forwarded to `ResourceType::set_value`. + /// @param args Arguments forwarded to `ResourceType::set_value`. template void set_value(Args&&... args) { value_.set_value(std::forward(args)...); } + /// @brief Creates a new resource at the given node using cloned function objects. + /// + /// The returned resource has a default-initialised value and exclusively-owned + /// function objects freshly created for `node_id`. + /// + /// @param node_id Target graph node identifier. + /// @return A new heap-allocated resource for `node_id`. [[nodiscard]] auto create(const size_t node_id) const -> std::unique_ptr { auto new_resource = std::make_unique(unique_dominance_function_->create(node_id), @@ -179,6 +286,11 @@ class ResourcePrototype { return new_resource; } + /// @brief Creates a new resource with the given value at the specified node. + /// + /// @param resource_value Initial value for the new resource (copied). + /// @param node_id Target graph node identifier. + /// @return A new heap-allocated resource initialised with `resource_value`. [[nodiscard]] auto create(const ResourceType& resource_value, const size_t node_id) const // NOLINT -> std::unique_ptr { @@ -192,7 +304,12 @@ class ResourcePrototype { return new_resource; } - // Create a new resource from a shallow copy of the current resource. + /// @brief Creates a new resource that borrows (shares) this resource's function objects. + /// + /// The returned resource does **not** own the function objects; the caller must ensure + /// that this resource outlives the returned copy. + /// + /// @return A heap-allocated resource with a shallow copy of the function pointers. [[nodiscard]] auto copy() const -> std::unique_ptr { auto new_resource = std::make_unique(dominance_function_, feasibility_function_, @@ -202,6 +319,11 @@ class ResourcePrototype { return new_resource; } + /// @brief Resets this resource to the initial state for a given node. + /// + /// Resets the stored value and propagates the reset to all three function objects. + /// + /// @param node_id Graph node identifier to re-initialise for. void reset(const size_t node_id) { value_.reset(); @@ -213,6 +335,12 @@ class ResourcePrototype { } // Reset the resource and copy the function objects from the resource passed as argument. + /// @brief Resets the value and rebinds the function-object pointers from another resource. + /// + /// After this call, the three function pointers point to the function objects of + /// `resource`. No ownership transfer occurs. + /// + /// @param resource Source resource whose function pointers are adopted. void reset(const ResourceClass& resource) { value_.reset(); diff --git a/cpp/rcspp/resource/base/resource_type.hpp b/cpp/rcspp/resource/base/resource_type.hpp index c257df7d..6d298bbb 100644 --- a/cpp/rcspp/resource/base/resource_type.hpp +++ b/cpp/rcspp/resource/base/resource_type.hpp @@ -5,6 +5,12 @@ namespace rcspp { +/// @brief Concept that constrains types usable as a resource value in the RCSPP framework. +/// +/// A type satisfies `ResourceTypeConcept` if it exposes the four operations required by the +/// resource management layer: `reset()`, `get_value()`, `set_value()`, and `to_string()`. +/// +/// @tparam ResourceType The candidate type to check against this concept. template concept ResourceTypeConcept = requires(ResourceType t) { t.reset(); diff --git a/cpp/rcspp/resource/composition/composition.hpp b/cpp/rcspp/resource/composition/composition.hpp index be2ea544..894484aa 100644 --- a/cpp/rcspp/resource/composition/composition.hpp +++ b/cpp/rcspp/resource/composition/composition.hpp @@ -12,19 +12,33 @@ namespace rcspp { -// compute index of first type in ResourceTypes... that is ResourceType -// Base template +/// @brief Compile-time index of @p ComponentType within the type list @p ComponentTypes. +/// +/// Provides a `value` equal to the zero-based position of the first occurrence of +/// @p ComponentType in @p ComponentTypes, or `-1` if it is not present. +/// +/// @tparam ComponentType The type to search for. +/// @tparam ComponentTypes The ordered list of types to search within. template struct ComponentTypeIndex; -// default case: not found +/// @brief Base case: empty list — @p ComponentType was not found. +/// +/// @tparam ComponentType The type being searched for. template struct ComponentTypeIndex { + /// @brief Sentinel value indicating the type was not found. static constexpr int value = -1; }; -// recursive case: either found at current index (ResourceType == ResourceType1) -// or continue searching in ResourceTypes... +/// @brief Recursive case: compare @p ComponentType against the front of the list. +/// +/// Returns 0 if @p ComponentType matches @p FrontComponentType; otherwise returns +/// one plus the index found in the remaining @p ComponentTypes (or -1 if not found). +/// +/// @tparam ComponentType The type to search for. +/// @tparam FrontComponentType The first type in the current list. +/// @tparam ComponentTypes The remaining types in the list. template struct ComponentTypeIndex { private: @@ -32,50 +46,79 @@ struct ComponentTypeIndex static constexpr int next_or_minus_one = (next == -1 ? -1 : 1 + next); public: + /// @brief Zero-based index of @p ComponentType, or -1 if not present. static constexpr int value = std::is_same_v ? 0 : next_or_minus_one; }; -// Retrieve the value of the associated template +/// @brief Convenience variable template for `ComponentTypeIndex::value`. +/// +/// Only participates in overload resolution when @p ComponentType is present in +/// @p ComponentTypes (i.e., the index is not -1). +/// +/// @tparam ComponentType The type to locate. +/// @tparam ComponentTypes The ordered list of types to search within. template requires(ComponentTypeIndex::value != -1) inline constexpr int ComponentTypeIndex_v = ComponentTypeIndex::value; -// ComponentType to ComponentInitializerTypeTuple -// Extracts the initializer type tuple for a given ComponentType -// Default implementation deduces the value type from ComponentType::get_value(), if any -// Specializations can be provided for more complex ComponentType that have initializer with several -// values -// default trait (can be specialized) +/// @brief Trait that maps a @p ComponentType to its initializer tuple type. +/// +/// The default implementation deduces the initializer as a one-element tuple +/// wrapping the decayed return type of `ComponentType::get_value()`. +/// Specializations may be provided for component types whose initializers +/// require multiple values. +/// +/// @tparam ComponentType A type that exposes a `get_value()` member function. template requires requires { std::declval().get_value(); } struct ComponentInitializerTypeTuple { + /// @brief The initializer tuple type for @p ComponentType. using type = std::tuple().get_value())>>; }; -// convenience alias +/// @brief Convenience alias for `ComponentInitializerTypeTuple::type`. +/// +/// @tparam ComponentType A type that satisfies the `ComponentInitializerTypeTuple` trait. template using ComponentInitializerTypeTuple_t = typename ComponentInitializerTypeTuple::type; -// tag base to identify all Composition specializations +/// @brief Empty tag base class used to identify all `Composition` specializations. struct CompositionTag {}; -// concept to test for any Composition<...> +/// @brief Concept that checks whether @p T is any specialization of `Composition`. +/// +/// @tparam T The type to test. template concept IsComposition = std::derived_from, CompositionTag>; -// Composition class that can hold multiple types of components +/// @brief Heterogeneous container that holds one vector of `ComponentClass` per base +/// type. +/// +/// Each entry in @p BaseTypes corresponds to a `std::vector>>` +/// stored in an internal tuple. The class provides iteration helpers (`apply`, +/// `for_each_component`) that dispatch a callable across every component-type slot simultaneously. +/// +/// @tparam ComponentClass A class template whose single template parameter is a base type. +/// @tparam BaseTypes The ordered list of base types that parameterise @p ComponentClass. template