Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,13 @@
docker/redis*
docker/production.rb
.env

ext/**/target/
/exe/

# Python cache
__pycache__/
*.pyc
*.pyo
venv/
.venv/
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,14 @@ TAGS
docker/redis*
docker/production.rb
.env

ext/**/target/
/exe/

__pycache__/
*.pyo
.Python
venv/
.venv/
*.egg-info/
.eggs/
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,17 @@ WORKDIR /srv/app

RUN apt update && \
libgeos=$(apt-cache search 'libgeos-' | grep -P 'libgeos-\d.*' | awk '{print $1}') && \
apt install -y git libgeos-dev ${libgeos} libicu-dev libglpk-dev nano
apt install -y git libgeos-dev ${libgeos} libicu-dev libglpk-dev nano cargo

ADD ./Gemfile /srv/app/
ADD ./Gemfile.lock /srv/app/
RUN bundle install --full-index --without ${BUNDLE_WITHOUT}

ADD . /srv/app

# Build vrp_delaunay Rust binary (Delaunay triangulation for graph endpoint)
RUN bundle exec rake ext:vrp_delaunay 2>/dev/null || true

EXPOSE 80

HEALTHCHECK \
Expand Down
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ gem 'polylines'
gem 'rgeo'
gem 'rgeo-geojson', require: 'rgeo/geo_json'

# Graph partitioning / clustering
gem 'graph-clustering', git: 'https://github.com/braktar/graph-clustering', glob: 'ruby/*.gemspec'

gem 'sentry-resque'
gem 'sentry-ruby'

Expand Down
8 changes: 8 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
GIT
remote: https://github.com/braktar/graph-clustering
revision: 0ba22c3a2dbf8c8e57e2ae4afae42f9034ccf853
glob: ruby/*.gemspec
specs:
graph-clustering (0.1.0)

GIT
remote: https://github.com/cartoway/rubocop-policy.git
revision: 209bde9a8088d4204b22829b2a9150860c16dcac
Expand Down Expand Up @@ -312,6 +319,7 @@ DEPENDENCIES
grape-swagger
grape-swagger-entity
grape_logging
graph-clustering!
http_accept_language
i18n
minitest
Expand Down
17 changes: 17 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,20 @@ end
task :environment do
require './environment'
end

# VrpDelaunay Rust binary (Spade)
namespace :ext do
desc 'Compile VrpDelaunay binary (Spade - requires cargo)'
task :vrp_delaunay do
ext_dir = File.expand_path('ext/vrp_delaunay', __dir__)
Dir.chdir(ext_dir) do
sh 'cargo build --release'
bin = File.join(ext_dir, 'target/release/vrp_delaunay')
raise 'Build failed: no binary produced' unless File.executable?(bin)

dest = File.expand_path('exe', __dir__)
FileUtils.mkdir_p(dest)
FileUtils.cp(bin, File.join(dest, 'vrp_delaunay'))
end
end
end
61 changes: 61 additions & 0 deletions api/v01/vrp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,67 @@ class Vrp < APIBase
::Models.delete_all
end

resource :graph do
desc 'Get Delaunay graph from VRP', {
nickname: 'get_vrp_graph',
success: [{
code: 200,
message: 'Graph built from VRP'
}],
failure: [{
code: 400,
message: 'Bad Request',
model: ::Api::V01::Status
}, {
code: 501,
message: 'vrp_delaunay binary not built (rake ext:vrp_delaunay)',
model: ::Api::V01::Status
}],
detail: 'Build and return the proximity graph (Delaunay, skills/timewindow compatibilities, K-NN) from a VRP instance.'
}
params {
use(:input)
optional(:format, type: Symbol, values: [:json, :geojson], default: :json,
desc: 'Output format: json (full graph) or geojson')
}
post do
d_params = declared(params, include_missing: false)
vrp_params = d_params[:points] ? d_params : d_params[:vrp]
vrp = ::Models::Vrp.create(vrp_params)

if !vrp.valid? || vrp_params.nil? || vrp_params.keys.empty?
vrp.errors.add(:empty_file, message: 'JSON file is empty') if vrp_params.nil?
vrp.errors.add(:empty_vrp, message: 'VRP structure is empty') if vrp_params&.keys&.empty?
error!("Model Validation Error: #{vrp.errors}", 400)
end

graph =
begin
VrpGraph::GraphBuilder.new(vrp).build
rescue LoadError => e
error!({ message: "Graph build failed: #{e.message}" }, 501)
end

if graph
vrp.graph = graph
end

if graph.nil?
error!({ message: 'No services with points to build graph' }, 400)
end

if params[:format] == :geojson
content_type 'application/vnd.geo+json'
graph.to_geojson
else
status 200
present graph.to_hash
end
ensure
::Models.delete_all
end
end

resource :jobs do
desc 'Fetch vrp job status', {
nickname: 'get_job',
Expand Down
8 changes: 8 additions & 0 deletions config/initializers/vrp_delaunay.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

# Build vrp_delaunay Rust binary at startup if missing (server + workers).
# Graph endpoint returns 501 when binary is absent; this avoids manual rake ext:vrp_delaunay.

require File.expand_path('../../lib/vrp_graph/ensure_binary', __dir__)

VrpGraph::EnsureBinary.ensure_built!
5 changes: 3 additions & 2 deletions core/strategies/orchestration.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require './lib/interpreters/re_partition'

module Core
module Strategies
module Orchestration
Expand Down Expand Up @@ -92,9 +94,8 @@ def define_process(service_vrp, job = nil, &block)
expected_activity_count = vrp.visits

# Calls define_process recursively
solution ||= Interpreters::RePartition.repartition(service_vrp, job, &block)
solution ||= Interpreters::SplitClustering.split_clusters(service_vrp, job, &block)
# Calls define_process recursively
solution ||= Interpreters::Dichotomous.dichotomous_heuristic(service_vrp, job, &block)

solution ||= Interpreters::MultiTrip.presolve(service_vrp, job, &block)

Expand Down
52 changes: 52 additions & 0 deletions docs/Graph.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Graph

Build and retrieve the proximity graph (Delaunay triangulation, skills/timewindow compatibilities, K-NN) from a VRP instance.

## Endpoint

**POST** `/0.1/vrp/graph`

Same request body as [submit](Home.md#submit). Additional parameters:

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `format` | String | `"json"` | Output format: `"json"` (full graph structure) or `"geojson"` |

## Example request

```bash
curl -X POST "http://localhost:1791/0.1/vrp/graph?api_key=your_key" \
-H "Content-Type: application/json" \
-d '{"vrp": {...}}'
```

## Response (format=json)

```json
{
"nodes": {
"service_1": {
"point": {"lat": 48.1, "lon": -1.6},
"skills": [],
"timewindows": [{"start": 0, "end": 86400}]
}
},
"edges": [["service_1", "service_2", 120], ...],
"incompatibilities": [["service_a", "service_b"], ...],
"knn_neighbors": {
"service_1": ["service_2", "service_3", ...]
},
"metadata": {
"delaunay_built_at": "2025-01-29T...",
"matrix_id_used": "..."
}
}
```

## Response (format=geojson)

Returns a GeoJSON FeatureCollection with Point features for each service and LineString features for each edge.

## Requirements

The `vrp_delaunay` binary must be built: `rake ext:vrp_delaunay`
Loading
Loading