Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
ae84696
Move the list of non-dominated labels to a Bucket class.
legraina Mar 6, 2026
1e06699
Code running correctly, but still some bugs on the dominance and the …
legraina Mar 9, 2026
8ab3791
Working Buckets.
legraina Mar 10, 2026
8beb230
Moved print labels to TRACE level.
legraina Mar 10, 2026
722c718
Change ResourceType to ContainerResourceType.
legraina Mar 10, 2026
969e51a
Fix all the errors with LabelContainerType to make it work for vrp an…
legraina Mar 10, 2026
ef27efc
Resolve copilot comments.
legraina Mar 10, 2026
0f50c64
Fix format for fast_domiance_check
legraina Mar 10, 2026
6cf7447
Fix adjency matrix in connectivity.
legraina Mar 10, 2026
917211e
Fix some bug in greedy. Also fix make_resource() and reset().
legraina Apr 22, 2026
6a2e782
Change make_resource to create_resource
legraina Apr 22, 2026
aac8b21
Merged
legraina Apr 23, 2026
f467784
Add the storage of the best bound, and it's pruning based on it.
legraina Apr 23, 2026
3e20952
Refactor: BacktrackingDiveAlgorithm + TabuList; add TabuSearchAlgorithm
legraina Apr 29, 2026
46ccc24
Add a timeout on the algorithms, add a leq comparison for the contain…
legraina May 5, 2026
dca8801
feat: path vectors, CI Python matrix, and py.typed stubs
legraina May 25, 2026
b4c97c1
perf: LabelBuckets — vector + binary search, O(1) erase, symmetric stats
legraina May 25, 2026
ff7b940
test+bench: LabelBuckets tests and BucketS/BucketP benchmark columns
legraina May 25, 2026
109e34e
Has implemented some improvements for bucket
legraina May 25, 2026
abe93e7
feat: unified benchmark solve with cross-checking, optional Boost, fo…
legraina May 25, 2026
1cde86d
feat: parent-pointer path reconstruction + AlgorithmBaseParams::with_…
legraina May 25, 2026
7ead13d
refactor: simplify benchmark labels vector construction
legraina May 25, 2026
2f4934a
feat: benchmark improvements — cost/BKS table, large instances, CI wo…
legraina May 25, 2026
339d523
Avoid copying solution vectors in collect_solutions
Copilot May 27, 2026
c5c3f4c
feat: memory-limit and pressure-pruning for label algorithms
legraina May 27, 2026
e74e7a5
Improve _add_rows_bulk: batch inserts per arc using sorted arc_id runs
legraina May 27, 2026
fb467e5
Demote resolve() memory-limit log from INFO to DEBUG
legraina May 27, 2026
20c254c
Add process RSS to memory-limit debug log
legraina May 27, 2026
1dae09f
merge: integrate tight-lb into quick-improvements
legraina May 27, 2026
b442c47
Merge branch 'quick-improvements' of github.com:lab-core/rcspp into q…
legraina May 27, 2026
6bcd31c
Clean files and minor fix.
legraina May 27, 2026
30cc88d
feat: add SolveResult, AlgorithmStatus, timeout, and release_after_so…
legraina May 28, 2026
f58ae8a
chore: update .gitignore and remove tracked build artifacts
legraina May 28, 2026
057cd1c
Add memory helpers in python.
legraina May 28, 2026
ecb81ad
Enforce timeout_s in TabuSearchAlgorithm::main_loop
legraina May 28, 2026
a4c3e9b
feat: add AStarDominanceAlgorithm with backward Bellman-Ford heuristic
legraina May 28, 2026
2193a5d
feat: use CostResourceType reduced costs for A* heuristic
legraina May 28, 2026
56c7f9b
fix: use AStarAlgoBound in VRP benchmarks
legraina May 28, 2026
ad00540
feat: add ImprovingTabuSearch, ConstructiveTabu/Tabu heuristics, --as…
legraina May 28, 2026
326e3b5
refactor: replace ImprovingTabuSearch with proper classical improving TS
legraina May 28, 2026
0a3cc8a
chore: use should_stop() consistently in tabu search algorithms
legraina May 29, 2026
08086ef
Ensure should_stop() is always used.
legraina May 29, 2026
c63b923
Merge branch 'main' into quick-improvements
legraina Jun 8, 2026
be5bc16
Removed idea files
legraina Jun 8, 2026
bf8ee8c
Poll timeout/interrupt inside dive loops to bound pathological solves
legraina Aug 1, 2026
30b1ba3
Poll the memory limit inside dive loops, not just timeout/interrupt
legraina Aug 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: Benchmark

on:
workflow_dispatch:
inputs:
max_instances:
description: 'Instances per family for small benchmark (1–9; default 2)'
default: '2'
required: true
type: string
run_large:
description: 'Also run large benchmark (C2/R2/RC2)'
type: boolean
default: false
max_large_instances:
description: 'Instances per family for large benchmark (1–8; R2 up to 11)'
default: '2'
required: false
type: string

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

defaults:
run:
shell: bash

jobs:
benchmark:
name: Benchmark / ubuntu
# NOTE: Gurobi must be installed on the runner and GUROBI_HOME must be set.
# Use a self-hosted runner with Gurobi, or adjust the "Install Gurobi" step.
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
with:
submodules: recursive
lfs: true

- name: Install build dependencies
run: sudo apt-get update -q && sudo apt-get install -y build-essential cmake

- name: Configure (Release, no Python, no tests)
run: |
cmake -B build \
-DCMAKE_BUILD_TYPE=Release \
-DUSE_PYTHON=OFF \
-DUSE_TESTS=OFF

- name: Build small benchmark
run: cmake --build build --target rcspp-vrp-benchmark --parallel

- name: Build large benchmark
if: inputs.run_large
run: cmake --build build --target rcspp-vrp-benchmark-large --parallel

- name: Run small benchmark (C1/R1/RC1)
run: |
./build/bin/rcspp-vrp-benchmark ${{ inputs.max_instances }} \
2>&1 | tee benchmark_small.txt

- name: Run large benchmark (C2/R2/RC2)
if: inputs.run_large
run: |
./build/bin/rcspp-vrp-benchmark-large ${{ inputs.max_large_instances }} \
2>&1 | tee benchmark_large.txt

- name: Upload small benchmark results
uses: actions/upload-artifact@v4
with:
name: benchmark-small-${{ github.sha }}
path: benchmark_small.txt

- name: Upload large benchmark results
if: inputs.run_large
uses: actions/upload-artifact@v4
with:
name: benchmark-large-${{ github.sha }}
path: benchmark_large.txt
13 changes: 10 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,20 @@ defaults:

jobs:
test:
name: Test / ${{ matrix.os }} / ${{ matrix.build-type }}
name: Test / ${{ matrix.os }} / ${{ matrix.build-type }} / py${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
build-type: [Debug, Release]
python-version: ['3.11', '3.12', '3.13']
exclude:
# Debug builds don't run Python tests — only one Python version needed
- build-type: Debug
python-version: '3.11'
- build-type: Debug
python-version: '3.13'

steps:
- uses: actions/checkout@v4
Expand All @@ -42,7 +49,7 @@ jobs:

- uses: actions/setup-python@v5
with:
python-version: '3.12'
python-version: ${{ matrix.python-version }}

- name: Configure
run: cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} -DUSE_PYTHON=ON -DUSE_TESTS=ON
Expand All @@ -60,7 +67,7 @@ jobs:
- name: Python tests
if: matrix.build-type == 'Release'
working-directory: src/python
run: pytest -v
run: pytest -v --tb=short

build_wheels:
name: Wheels / ${{ matrix.os }}
Expand Down
18 changes: 17 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
.vs/*
.cache/*
out/*
build/*
build/
build_*/
cmake-build-*/
**/__pycache__/**
*.dll
*.lib
*.exp
Expand All @@ -9,8 +13,14 @@ build/*
*.o
*.a
*.exe
*.so
*.dylib
instances/duals/

# Python
.venv/
.pytest_cache/

# CMake / Ninja generated files
CMakeCache.txt
CMakeFiles/
Expand All @@ -37,3 +47,9 @@ INSTALL*
# Per-target/build subdirectories generated by CMake/VS
*.dir/
Testing/

# Claude Code project files
.claude/

# Clion
.idea/
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ repos:
hooks:
- id: markdownlint
files: .*\.md$
exclude: ^(build|build_[^/]+)/
args: ["--fix"]

# -------------------------
Expand Down
3 changes: 3 additions & 0 deletions instances/RC201_12.txt
Git LFS file not shown
21 changes: 12 additions & 9 deletions src/python/test_clone.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
- next_arc_id() returns the expected next arc ID
- add_rows_to_arc buffers rows and flushes them correctly
- add_rows adds rows in bulk from a list of triples or a 2-D numpy array
- update_reduced_costs uses stored dual rows to compute reduced costs
- update_reduced_costs uses stored rows to compute reduced costs
- clone() produces an independent copy with stable arc IDs and preserved rows
- clone_topology() produces an independent copy with empty dual rows
- clone_topology() produces an independent copy with empty rows
- clone_removed_arcs=True preserves removed arcs in the clone
- Removing arcs in a clone does not affect the original graph
"""
Expand All @@ -18,10 +18,13 @@
import os
import sys

import numpy as np
sys.path.insert(
0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "python_interface")
)

import pytest # noqa: E402

relative_path = "../python_interface/"
sys.path.insert(0, os.path.abspath(relative_path))
np = pytest.importorskip("numpy")

from rcspp.graph import ResourceGraph # noqa: E402
from rcspp.resource import ( # noqa: E402
Expand Down Expand Up @@ -62,7 +65,7 @@ def _make_graph() -> ResourceGraph:
# ── add_arc with rows variants ──────────────────────────────────────────


class TestAddArcDualRows:
class TestAddArcRows:
"""Test rows normalisation in add_arc."""

def test_single_tuple(self):
Expand Down Expand Up @@ -261,7 +264,7 @@ def test_multiple_rows_per_arc(self):


class TestUpdateReducedCosts:
"""Test that update_reduced_costs correctly applies dual rows."""
"""Test that update_reduced_costs correctly applies rows."""

def test_rows_shift_optimal_path(self):
"""After update_reduced_costs, the path with negative reduced cost wins.
Expand Down Expand Up @@ -327,7 +330,7 @@ class TestClone:
"""Test ResourceGraph.clone()."""

def test_clone_preserves_rows(self):
"""Clone() carries dual rows into the copy."""
"""Clone() carries rows into the copy."""
rg = _make_graph()
arc0 = rg.add_arc((10.0,), 0, 1, cost=10.0, rows=(0, 2.0))
rg.add_arc((15.0,), 1, 3, cost=15.0)
Expand Down Expand Up @@ -426,7 +429,7 @@ def test_clone_independent_reduced_costs(self):


class TestCloneTopology:
"""Test ResourceGraph.clone_topology() — clone with no dual rows."""
"""Test ResourceGraph.clone_topology() — clone with no rows."""

def test_rows_stripped(self):
"""clone_topology() produces arcs with empty rows."""
Expand Down
11 changes: 7 additions & 4 deletions src/python/test_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
import os
import sys

import numpy as np
sys.path.insert(
0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "python_interface")
)

import pytest # noqa: E402

relative_path = "../python_interface/"
sys.path.insert(0, os.path.abspath(relative_path))
np = pytest.importorskip("numpy")

from rcspp.graph import ResourceGraph # noqa: E402
from rcspp.resource import ( # noqa: E402
Expand Down Expand Up @@ -179,7 +182,7 @@ def test_force_arc_solve_uses_forced_path():


def _make_rg_with_rows():
"""2-arc graph where arc costs are set via dual rows.
"""2-arc graph where arc costs are set via rows.

Arc 0 (0→1): base cost=10, row index=0 coef=1 → reduced = 10 - duals[0]
Arc 1 (1→2): base cost=20, row index=1 coef=2 → reduced = 20 - 2*duals[1]
Expand Down
50 changes: 46 additions & 4 deletions src/python/test_rcspp.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
import threading
import time

relative_path = "../python_interface/"
sys.path.insert(0, os.path.abspath(relative_path))
sys.path.insert(
0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "python_interface")
)

from rcspp import LogLevel, set_log_level
from rcspp.graph import Algorithm, AlgorithmParams, BucketAlgorithmParams, ResourceGraph
Expand Down Expand Up @@ -221,21 +222,27 @@ def example_algorithm_params():
rg.add_arc((3.0,), 1, 2, cost=3.0)
rg.add_arc((10.0,), 0, 2, cost=10.0)

# Enum values: Algorithm.Simple, Algorithm.Pushing, Algorithm.Pulling, Algorithm.Greedy
# Enum values: Algorithm.Simple, Algorithm.Pushing, Algorithm.Pulling, Algorithm.Greedy, Algorithm.AStar
sols_simple = rg.solve(Algorithm.Simple)
sols_pushing = rg.solve(Algorithm.Pushing)
sols_pulling = rg.solve(Algorithm.Pulling)
sols_greedy = rg.solve(Algorithm.Greedy)
sols_astar = rg.solve(Algorithm.AStar)
# String aliases are also accepted for convenience
sols_str = rg.solve("simple")
sols_astar_str = rg.solve("astar")

print_solutions("Algorithm.Simple", sols_simple)
print_solutions("Algorithm.Pushing", sols_pushing)
print_solutions("Algorithm.Pulling", sols_pulling)
print_solutions("Algorithm.Greedy", sols_greedy)
print_solutions("Algorithm.AStar", sols_astar)

assert sols_simple[0].cost == sols_str[0].cost, "string alias must match enum"
assert all(s.cost == 8.0 for s in [sols_simple[0], sols_pulling[0], sols_greedy[0]])
assert sols_astar[0].cost == sols_astar_str[0].cost, "astar string alias must match enum"
assert all(
s.cost == 8.0 for s in [sols_simple[0], sols_pulling[0], sols_greedy[0], sols_astar[0]]
)

# AlgorithmParams: stop after the first solution
params = AlgorithmParams()
Expand Down Expand Up @@ -782,6 +789,41 @@ def example_bucket_labels():
sols_one = rg.solve(params=bp_inh)
assert len(sols_one) == 1, f"Expected 1 solution, got {len(sols_one)}"

# ── Position-based API: bucket by int (pos 1), sort by real (pos 0) ───────
# rg2 has real at registration pos 0 and int at pos 1.
# bucket_resource_pos=1 → int resource; sort_resource_pos=0 → real (cost).
bp_pos = BucketAlgorithmParams(
range_buckets=3,
bucket_resource_pos=1,
sort_resource_pos=0,
)
sols_pos = rg2.solve(params=bp_pos)
print_solutions("BucketAlgorithmParams pos-based (bucket=int, sort=real)", sols_pos)
assert sols_pos, "bucket solve (pos-based) returned no solutions"
assert math.isclose(
sols_pos[0].cost, ref2_cost, abs_tol=1e-6
), f"bucket pos-based cost {sols_pos[0].cost} != reference {ref2_cost}"

# Bucket by real (pos 0), sort by real (pos 0) — same type, first instance.
bp_pos_real = BucketAlgorithmParams(
range_buckets=5,
bucket_resource_pos=0,
sort_resource_pos=0,
)
sols_pos_real = rg2.solve(params=bp_pos_real)
assert sols_pos_real, "bucket solve (pos 0,0) returned no solutions"
assert math.isclose(
sols_pos_real[0].cost, ref2_cost, abs_tol=1e-6
), f"bucket pos(0,0) cost {sols_pos_real[0].cost} != reference {ref2_cost}"

# Out-of-range position raises ValueError.
bp_oob = BucketAlgorithmParams(bucket_resource_pos=99)
try:
rg2.solve(params=bp_oob)
assert False, "Expected ValueError for out-of-range bucket_resource_pos"
except ValueError:
pass # expected


# ── Run all examples ──────────────────────────────────────────────────────────

Expand Down
5 changes: 3 additions & 2 deletions src/python/test_rcspp_networkx.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@

import networkx as nx

relative_path = "../python_interface/"
sys.path.insert(0, os.path.abspath(relative_path))
sys.path.insert(
0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "python_interface")
)

from rcspp.graph import Algorithm, AlgorithmParams, ResourceGraph
from rcspp.resource import (
Expand Down
6 changes: 4 additions & 2 deletions src/python/test_vrp.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
import random
import sys

sys.path.insert(0, os.path.abspath("../python_interface/"))
sys.path.insert(0, os.path.abspath("."))
sys.path.insert(
0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "python_interface")
)
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from vrp.instance import Customer, Instance

Expand Down
11 changes: 8 additions & 3 deletions src/python_interface/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,12 @@ target_include_directories(${PYTHON_EXTENSION_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/
# Link the core C++ library
target_link_libraries(${PYTHON_EXTENSION_NAME} PRIVATE rcspp_objects)

# Collect .py files from the C++/pybind11 interface directory (incl. pricing_pool.py,
# which lives alongside _core so a single `rcspp` package exposes both).
file(GLOB_RECURSE PYTHON_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/rcspp/*.py")
# Find all .py, .pyi, and py.typed files in rcspp (recursively)
file(GLOB_RECURSE PYTHON_SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/rcspp/*.py"
"${CMAKE_CURRENT_SOURCE_DIR}/rcspp/*.pyi"
"${CMAKE_CURRENT_SOURCE_DIR}/rcspp/py.typed"
)

# Build per-file copy rules with explicit OUTPUT so CMake re-copies whenever a
# .py file changes, without requiring the C++ extension to rebuild.
Expand Down Expand Up @@ -84,4 +87,6 @@ install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/rcspp/"
DESTINATION ${PYTHON_PACKAGE_NAME}
FILES_MATCHING
PATTERN "*.py"
PATTERN "*.pyi"
PATTERN "py.typed"
)
Loading