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
21 changes: 21 additions & 0 deletions tags_constraint_solution.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
--- Tags Constraint Example Results ---
Total routes: 2
Assigned jobs: 4
Unassigned jobs: 0

Route 1:
Vehicle: v1
Jobs:
- job1
- job2

Route 2:
Vehicle: v2
Jobs:
- job4
- job3

Expected behavior:
- job1 and job2 (fragile) should be assigned to vehicle_1 (has fragile tag)
- job3 and job4 (no tags) can be assigned to any vehicle
- vehicle_2 (no tags) cannot serve job1 or job2
203 changes: 203 additions & 0 deletions vrp-core/examples/tags_constraint_example.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
//! This example demonstrates how to use a custom tags constraint to restrict
//! job assignments based on vehicle capabilities.
//!
//! In this scenario:
//! - job1 and job2 require a "fragile" tag (can only be served by vehicles with "fragile" tag)
//! - job3 and job4 have no tag requirements (can be served by any vehicle)
//! - vehicle_1 has both "fragile" and "hazmat" tags
//! - vehicle_2 has no tags (so cannot serve fragile jobs)
//!
//! Key points of implementation:
//! - using custom dimension (property) for jobs and vehicles with dedicated custom_dimension macro
//! - assigning tags to the jobs and vehicles by setting custom properties accordingly
//! - using the provided create_tags_feature which validates job-vehicle tag compatibility

#[path = "./common/routing.rs"]
mod common;
use crate::common::define_routing_data;

use std::collections::HashSet;
use std::sync::Arc;
use std::fs::File;
use std::io::Write;
use vrp_core::prelude::*;
use vrp_core::construction::features::{
CapacityFeatureBuilder, TransportFeatureBuilder, create_tags_feature,
JobTagsDimension, VehicleTagsDimension,
};
use vrp_core::models::problem::{JobIdDimension, VehicleIdDimension};

/// Specifies a CVRP problem variant with tags constraint: 4 delivery jobs (2 with "fragile" tag) and 2 vehicles
fn define_problem(goal: GoalContext, transport: Arc<dyn TransportCost>) -> GenericResult<Problem> {
// create 4 jobs: 2 with "fragile" tag, 2 without
let single_jobs = (1..=5)
.map(|idx| {
SingleBuilder::default()
.id(format!("job{idx}").as_str())
.demand(Demand::delivery(1))
.dimension(|dimens| {
// jobs 1 and 2 have fragile requirement
if idx <= 2 {
let mut tags = HashSet::new();
tags.insert("fragile".to_string());
dimens.set_job_tags(tags);
}
if idx == 5 {
let mut tags = HashSet::new();
tags.insert("hazmat".to_string());
dimens.set_job_tags(tags);
}
})
.location(idx)?
.build_as_job()
})
.collect::<Result<Vec<_>, _>>()?;

// create 2 vehicles
let vehicles = (1..=2)
.map(|idx| {
VehicleBuilder::default()
.id(format!("v{idx}").as_str())
.add_detail(
VehicleDetailBuilder::default()
.set_start_location(0)
.set_end_location(0)
.build()?,
)
.dimension(|dimens| {
// only vehicle 1 has "fragile" tag capability
if idx == 1 {
let mut tags = HashSet::new();
tags.insert("fragile".to_string());
tags.insert("hazmat".to_string());
dimens.set_vehicle_tags(tags);
}
})
// each vehicle has capacity=2, so it can serve at most 2 jobs
.capacity(SingleDimLoad::new(2))
.build()
})
.collect::<Result<Vec<_>, _>>()?;

let vehicle3 = VehicleBuilder::default()
.id("v3")
.add_detail(
VehicleDetailBuilder::default()
.set_start_location(0)
.set_end_location(0)
.build()?,
)
.dimension(|dimens| {
// vehicle 3 has "hazmat" tag capability only
let mut tags = HashSet::new();
tags.insert("hazmat".to_string());
dimens.set_vehicle_tags(tags);
})
// each vehicle has capacity=2, so it can serve at most 2 jobs
.capacity(SingleDimLoad::new(2))
.build()?;

ProblemBuilder::default()
.add_jobs(single_jobs.into_iter())
.add_vehicles(vehicles.into_iter())
.add_vehicle(vehicle3)
.with_goal(goal)
.with_transport_cost(transport)
.build()
}

/// Defines CVRP variant with tags constraint as a goal of optimization.
fn define_goal(transport: Arc<dyn TransportCost>) -> GenericResult<GoalContext> {
let minimize_unassigned = MinimizeUnassignedBuilder::new("min-unassigned").build()?;
let capacity_feature = CapacityFeatureBuilder::<SingleDimLoad>::new("capacity").build()?;
let transport_feature = TransportFeatureBuilder::new("min-distance")
.set_transport_cost(transport)
.set_time_constrained(false)
.build_minimize_distance()?;
// create our custom tags feature
let tags_feature = create_tags_feature("tags", ViolationCode::from(2))?;

// configure goal of optimization
GoalContextBuilder::with_features(&[minimize_unassigned, transport_feature, capacity_feature, tags_feature])?
.build()
}

fn main() -> GenericResult<()> {
let transport = Arc::new(define_routing_data()?);

let goal = define_goal(transport.clone())?;
let problem = Arc::new(define_problem(goal, transport)?);

let config = VrpConfigBuilder::new(problem.clone()).prebuild()?.with_max_generations(Some(10)).build()?;

// run the VRP solver and get the best known solution
let solution = Solver::new(problem, config).solve()?;

// Create output file
let mut output = File::create("tags_constraint_solution.txt")?;

// Write results to file
writeln!(output, "--- Tags Constraint Example Results ---")?;
writeln!(output, "Total routes: {}", solution.routes.len())?;
writeln!(output, "Assigned jobs: {}", solution.routes.iter().map(|r| r.tour.job_count()).sum::<usize>())?;
writeln!(output, "Unassigned jobs: {}", solution.unassigned.len())?;
writeln!(output)?;

for (idx, route) in solution.routes.iter().enumerate() {
writeln!(output, "Route {}:", idx + 1)?;
writeln!(output, " Vehicle: {}", route.actor.vehicle.dimens.get_vehicle_id().unwrap_or(&"unknown".to_string()))?;
writeln!(output, " Jobs:")?;
for job in route.tour.jobs() {
writeln!(output, " - {}", job.dimens().get_job_id().unwrap_or(&"unknown".to_string()))?;
}
writeln!(output)?;
}

if !solution.unassigned.is_empty() {
writeln!(output, "Unassigned jobs (failed tag matching):")?;
for (job, _) in solution.unassigned.iter() {
writeln!(output, " - {}", job.dimens().get_job_id().unwrap_or(&"unknown".to_string()))?;
}
writeln!(output)?;
}

writeln!(output, "Expected behavior:")?;
writeln!(output, " - job1 and job2 (fragile) should be assigned to vehicle_1 (has fragile tag)")?;
writeln!(output, " - job3 and job4 (no tags) can be assigned to any vehicle")?;
writeln!(output, " - job5 (hazmat) can be assigned to vehicle_1 or vehicle_3 (have hazmat tag)")?;
writeln!(output, " - vehicle_2 (no tags) cannot serve job1 or job2")?;
writeln!(output, " - vehicle_3 (hazmat tag) cannot serve job1 or job2")?;

// Also print to console
println!("\n--- Tags Constraint Example Results ---");
println!("Total routes: {}", solution.routes.len());
println!("Assigned jobs: {}", solution.routes.iter().map(|r| r.tour.job_count()).sum::<usize>());
println!("Unassigned jobs: {}", solution.unassigned.len());
println!("\nResults written to: tags_constraint_solution.txt");

for (idx, route) in solution.routes.iter().enumerate() {
println!("\nRoute {}:", idx + 1);
println!(" Vehicle: {}", route.actor.vehicle.dimens.get_vehicle_id().unwrap_or(&"unknown".to_string()));
println!(" Jobs:");
for job in route.tour.jobs() {
println!(" - {}", job.dimens().get_job_id().unwrap_or(&"unknown".to_string()));
}
}

if !solution.unassigned.is_empty() {
println!("\nUnassigned jobs (failed tag matching):");
for (job, _) in solution.unassigned.iter() {
println!(" - {}", job.dimens().get_job_id().unwrap_or(&"unknown".to_string()));
}
}

println!("\nExpected behavior:");
println!(" - job1 and job2 (fragile) should be assigned to vehicle_1 (has fragile tag)");
println!(" - job3 and job4 (no tags) can be assigned to any vehicle");
println!(" - job5 (hazmat) can be assigned to vehicle_1 or vehicle_3 (have hazmat tag)");
println!(" - vehicle_2 (no tags) cannot serve job1 or job2");
println!(" - vehicle_3 (hazmat tag) cannot serve job1 or job2");


Ok(())
}
3 changes: 3 additions & 0 deletions vrp-core/src/construction/features/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ pub use self::reloads::{ReloadFeatureFactory, ReloadIntervalsTourState, SharedRe
mod skills;
pub use self::skills::{JobSkills, JobSkillsDimension, VehicleSkillsDimension, create_skills_feature};

mod tags;
pub use self::tags::{JobTagsDimension, VehicleTagsDimension, create_tags_feature};

mod total_value;
pub use self::total_value::*;

Expand Down
65 changes: 65 additions & 0 deletions vrp-core/src/construction/features/tags.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//! A job-vehicle tags feature.

#[cfg(test)]
#[path = "../../../tests/unit/construction/features/tags_test.rs"]
mod tags_test;

use super::*;
use std::collections::HashSet;

custom_dimension!(pub JobTags typeof HashSet<String>);
custom_dimension!(pub VehicleTags typeof HashSet<String>);

/// Creates a tags feature as hard constraint.
///
/// A job with tags can only be assigned to a vehicle that has all those tags.
/// If a job has no tags, any vehicle can service it.
pub fn create_tags_feature(name: &str, code: ViolationCode) -> Result<Feature, GenericError> {
FeatureBuilder::default().with_name(name).with_constraint(TagsConstraint { code }).build()
}

struct TagsConstraint {
code: ViolationCode,
}

impl FeatureConstraint for TagsConstraint {
fn evaluate(&self, move_ctx: &MoveContext<'_>) -> Option<ConstraintViolation> {
match move_ctx {
MoveContext::Route { route_ctx, job, .. } => {
if let Some(job_tags) = job.dimens().get_job_tags() {
if !job_tags.is_empty() {
let vehicle_tags = route_ctx.route().actor.vehicle.dimens.get_vehicle_tags();

// Vehicle must have all job tags
let has_required_tags = match vehicle_tags {
Some(v_tags) => job_tags.is_subset(v_tags),
None => false,
};

if !has_required_tags {
return ConstraintViolation::fail(self.code);
}
}
}

None
}
MoveContext::Activity { .. } => None,
}
}

fn merge(&self, source: Job, candidate: Job) -> Result<Job, ViolationCode> {
let source_tags = source.dimens().get_job_tags();
let candidate_tags = candidate.dimens().get_job_tags();

let has_compatible_tags = match (source_tags, candidate_tags) {
(None, None) | (Some(_), None) | (None, Some(_)) => true,
(Some(source_tags), Some(candidate_tags)) => {
// Both jobs must have the same tags to be merged
source_tags == candidate_tags
}
};

if has_compatible_tags { Ok(source) } else { Err(self.code) }
}
}
Loading