Vehicle Routing Problem with Time Windows#
Introduction#
In this example, we will explore how to model a Vehicle Routing Problem (VRP) with time windows using the DecisionAI interface. The VRP is a classic optimization problem where the goal is to determine the optimal set of routes for a fleet of vehicles to traverse in order to deliver to a given set of customers. This example will demonstrate how to define the problem’s inputs, variables, objective, and constraints within the DecisionAI framework.
To try out this base model in the CLI, you can run the following command:
export QUANTAGONIA_API_KEY=<YOUR_API_KEY>
python -m decision_ai.examples.vrp_time_windows.chat_example
Mathematical Model Description#
The VRP with time windows is designed to optimize the routes of vehicles such that all customer demands are met within specified time windows, while minimizing the total cost of the routes.
Inputs#
\(G(V, A)\): Directed graph where \(V = \{1, \ldots, n\}\) is the set of nodes, and \(A\) is the set of arcs.
\(N = V \cup \{0\}\cup \{n+1\}\): Nodes including the source (0) and sink (n+1).
\(q_0, \dots, q_{n+1}\): Demand of each node.
\(s_0, \dots, s_{n+1}\): Service time of each node.
\([a_i, b_i]\) for all \(i\in N\): Time windows for serving each node.
\(c_{ij}, t_{ij}\) for all \((i,j)\in A\): Cost and travel time for each edge.
\(K = \{1, \ldots, k\}\): Set of vehicles with uniform capacity \(Q\).
\(M_{ij}\): Big-M values for linearizing service time constraints.
Variables#
\(x_{ijk} \in \{0, 1\}\): Binary variable indicating if edge \((i,j)\) is traversed by vehicle \(k\).
\(T_{ik}\): Start of service at node \(i\) by vehicle \(k\).
Objective#
Minimize the total cost of the routes:
Constraints#
Customer Assignment: Each customer is assigned to exactly one route:
\[\sum_{k\in K} \sum_{j \in \delta^+(i)} x_{ijk} = 1, \quad \forall i \in N\]Source Departure: Each vehicle must leave the source:
\[\sum_{j \in \delta^+(0)} x_{0jk} = 1, \quad \forall k \in K\]Flow Conservation: Maintain flow conservation for each node:
\[\sum_{i \in \delta^-(j)} x_{ijk} - \sum_{i \in \delta^+(j)} x_{jik} = 0, \quad \forall j \in V, k \in K\]Sink Arrival: Each vehicle must enter the sink:
\[\sum_{i \in \delta^-(n+1)} x_{i,n+1,k} = 1, \quad \forall k \in K\]Service Time: Ensure service time constraints are met:
\[T_{ik} + s_i + t_{ij} - T_{jk} \leq (1-x_{ijk})M_{ij}, \quad \forall (i,j) \in A, k \in K, \text{ if } j \neq 0 \text{ and } i \neq n+1\]Time Windows: Respect time windows for each node:
\[a_i \leq T_{ik} \leq b_i, \quad \forall i \in V, k \in K\]Vehicle Capacity: Ensure vehicle capacity is not exceeded:
\[\sum_{i \in N} q_i \sum_{j \in \delta^+(i)} x_{ijk} \leq Q, \quad \forall k \in K\]
Model Implementation#
To implement the model, we first define the input data, variables, and constraints.
Input Data#
We start by defining the input data class.
class VRPTimeWindowsInput(InputData):
nodes: list[str]
edges: list[tuple[str, str]]
delta_out: dict[str, list[str]]
delta_in: dict[str, list[str]]
demand: dict[str, int]
latitudes: dict[str, float]
longitudes: dict[str, float]
service_time: dict[str, float]
time_window_start: dict[str, float]
time_window_end: dict[str, float]
travel_time: dict[str, dict[str, float]]
cost: dict[str, dict[str, float]]
vehicle_capacity: int
vehicles: list[str]
source: str
sink: str
@staticmethod
def from_csvs(path_to_directory: str | Path) -> VRPTimeWindowsInput:
"""Load VRP Time Windows input data from CSV files in the specified directory."""
path_to_directory = Path(path_to_directory)
# Load required CSV files
nodes_df = pd.read_csv(path_to_directory / "nodes.csv")
edges_df = pd.read_csv(path_to_directory / "edges.csv")
extra_df = pd.read_csv(path_to_directory / "extra.csv")
vehicles_df = pd.read_csv(path_to_directory / "vehicles.csv")
data = {"nodes": nodes_df, "edges": edges_df, "extra": extra_df, "vehicles": vehicles_df}
return VRPTimeWindowsInput.from_dataframes(data)
@staticmethod
def from_dataframes(data: dict[str, pd.DataFrame]) -> VRPTimeWindowsInput:
"""Convert pandas DataFrames into VRP Time Windows input data."""
nodes = data["nodes"]["node"].values.tolist()
source = nodes[0]
sink = nodes[-1]
demand = dict(zip(nodes, data["nodes"]["demand"], strict=False))
latitudes = dict(zip(nodes, data["nodes"]["lat"], strict=False))
longitudes = dict(zip(nodes, data["nodes"]["lon"], strict=False))
time_window_start = dict(zip(nodes, data["nodes"]["time_window_start"], strict=False))
time_window_end = dict(zip(nodes, data["nodes"]["time_window_end"], strict=False))
service_time = dict(zip(nodes, data["nodes"]["service_time"], strict=False))
edges, travel_time, cost = [], {}, {}
for _, row in data["edges"].iterrows():
from_, to_ = row["from"], row["to"]
edges.append((from_, to_))
edges.append((to_, from_))
# Create nested dictionary for travel_time
if from_ not in travel_time:
travel_time[from_] = {}
travel_time[from_][to_] = row["travel_time"]
if to_ not in travel_time:
travel_time[to_] = {}
travel_time[to_][from_] = row["travel_time"]
# Create a nested dictionary for cost
if from_ not in cost:
cost[from_] = {}
cost[from_][to_] = row["cost"]
if to_ not in cost:
cost[to_] = {}
cost[to_][from_] = row["cost"]
delta_in, delta_out = defaultdict(set), defaultdict(set)
for from_, to_ in edges:
delta_out[from_].add(to_)
delta_out[to_].add(from_)
delta_in[to_].add(from_)
delta_in[from_].add(to_)
for key in delta_in:
delta_in[key] = list(delta_in[key])
for key in delta_out:
delta_out[key] = list(delta_out[key])
vehicle_capacity = data["extra"][data["extra"]["field"] == "vehicle_capacity"]["value"].values[0]
vehicles = data["vehicles"]["vehicle"].values.tolist()
return VRPTimeWindowsInput(
nodes=nodes,
edges=edges,
delta_in=delta_in,
delta_out=delta_out,
demand=demand,
latitudes=latitudes,
longitudes=longitudes,
service_time=service_time,
time_window_start=time_window_start,
time_window_end=time_window_end,
travel_time=travel_time,
cost=cost,
vehicle_capacity=vehicle_capacity,
vehicles=vehicles,
source=source,
sink=sink,
)
@staticmethod
def load_example() -> VRPTimeWindowsInput:
"""Load example data.
Returns:
VRPTimeWindowsInput: The example data
"""
return VRPTimeWindowsInput.from_csvs(files(vrp_time_windows).joinpath("data_tables"))
The input data class inherits from decision_ai.InputData, which provides the basic structure for defining input data in DecisionAI models.
Variables#
We then define the model variables pydantic model. We include the initialization methods for each variable.
class VRPTimeWindowsVariables(PulpVariables):
# index (node1, node2, vehicle). 1 if vehicle travels node1 -> node2, 0 otherwise
x: dict[tuple[str, str, str], pulp.LpVariable] = Field(
..., description="Flow variables. 1 if vehicle travels node1 -> node2, 0 otherwise"
)
# index (node). Service start time at node
service_start_time: dict[str, pulp.LpVariable] = Field(..., description="Service start time at node")
@staticmethod
def init_x(input_: VRPTimeWindowsInput) -> dict[tuple[str, str, str], pulp.LpVariable]:
return {
(node1, node2, vehicle): pulp.LpVariable(f"x_{node1}_{node2}_{vehicle}", cat=pulp.LpBinary)
for node1, node2 in input_.edges
for vehicle in input_.vehicles
}
@staticmethod
def init_service_start_time(input_: VRPTimeWindowsInput) -> dict[str, pulp.LpVariable]:
return {node: pulp.LpVariable(f"service_start_time_{node}", cat=pulp.LpContinuous) for node in input_.nodes}
The variables class inherits from decision_ai.PulpVariables, which provides the framework for defining optimization variables in DecisionAI models.
Model Class#
Finally, we define the model class. We attach the variables class to the model class by assigning it to the variables_class attribute.
class VRPTimeWindowsModel(PulpDecisionAIModel[VRPTimeWindowsInput, VRPTimeWindowsVariables]):
variables_class = VRPTimeWindowsVariables
def __init__(self):
super().__init__()
def solution_to_str(self, input_: VRPTimeWindowsInput, solution: Solution) -> str:
solution_display = "##### Solution\n\n"
solution_display += f"Objective value: {solution.objective}\n\n"
solution_display += f"Status: {solution.status}\n\n"
if solution.status == "Infeasible":
return solution_display
# Calculate the routes for each vehicle
routes = {}
for k in input_.vehicles:
routes[k] = [str(input_.source)]
node = input_.source
while node != input_.sink:
for j in input_.delta_out[node]:
if solution.variables.x[node, j, k] > 0.5:
routes[k].append(f"{j!s} ({round(solution.variables.service_start_time[j], 1)})")
node = j
break
# Show the routes
solution_display += "Routes: (in parenthesis the starting service time)\n"
for k in input_.vehicles:
solution_display += f"\n - {k}: {' -> '.join(routes[k])}"
return solution_display
@constraint
def customer_visited_once(input_: VRPTimeWindowsInput, variables: VRPTimeWindowsVariables) -> ConstraintGenerator:
"""Each customer must be visited exactly once, except source and sink."""
for i in input_.nodes:
if i in [input_.source, input_.sink]:
continue
yield (
pulp.lpSum(variables.x[i, j, k] for k in input_.vehicles for j in input_.delta_in[i]) == 1,
f"Customer_assignment_{i}",
)
@constraint
def source_out(input_: VRPTimeWindowsInput, variables: VRPTimeWindowsVariables) -> ConstraintGenerator:
"""Each vehicle must leave source once and never enter it."""
for k in input_.vehicles:
yield (
pulp.lpSum(variables.x[input_.source, j, k] for j in input_.delta_out[input_.source]) == 1,
f"Source_leave_{k}",
)
yield (
pulp.lpSum(variables.x[j, input_.source, k] for j in input_.delta_in[input_.source]) == 0,
f"Source_enter_{k}",
)
@constraint
def flow_conservation(input_: VRPTimeWindowsInput, variables: VRPTimeWindowsVariables) -> ConstraintGenerator:
"""Flow must be conserved at each node except source and sink."""
for j in input_.nodes:
if j in [input_.source, input_.sink]:
continue
for k in input_.vehicles:
yield (
pulp.lpSum(variables.x[i, j, k] for i in input_.delta_in[j])
- pulp.lpSum(variables.x[j, i, k] for i in input_.delta_out[j])
== 0,
f"Flow_conservation_{j}_{k}",
)
@constraint
def sink_in(input_: VRPTimeWindowsInput, variables: VRPTimeWindowsVariables) -> ConstraintGenerator:
"""Each vehicle must enter sink once and never leave it."""
for k in input_.vehicles:
yield (
pulp.lpSum(variables.x[i, input_.sink, k] for i in input_.delta_in[input_.sink]) == 1,
f"Sink_enter_{k}",
)
yield (
pulp.lpSum(variables.x[input_.sink, j, k] for j in input_.delta_out[input_.sink]) == 0,
f"Sink_leave_{k}",
)
@constraint
def service_time(input_: VRPTimeWindowsInput, variables: VRPTimeWindowsVariables) -> ConstraintGenerator:
"""Service time constraints between consecutive nodes."""
T = variables.service_start_time
M = {
(i, j): max(
input_.time_window_end[i]
- input_.time_window_start[j]
+ input_.service_time[i]
+ input_.travel_time[i][j],
0,
)
for (i, j) in input_.edges
}
for i, j in input_.edges:
if j == input_.source or i == input_.sink:
continue
for k in input_.vehicles:
yield (
T[i] + input_.service_time[i] + input_.travel_time[i][j] - T[j]
<= (1 - variables.x[i, j, k]) * M[i, j],
f"Service_time_{i}_{j}_{k}",
)
@constraint
def time_window(input_: VRPTimeWindowsInput, variables: VRPTimeWindowsVariables) -> ConstraintGenerator:
"""Service must start within the time window for each node."""
for i in input_.nodes:
yield (input_.time_window_start[i] <= variables.service_start_time[i], f"Time_window_start_{i}")
yield (variables.service_start_time[i] <= input_.time_window_end[i], f"Time_window_end_{i}")
@constraint
def vehicle_capacity(input_: VRPTimeWindowsInput, variables: VRPTimeWindowsVariables) -> ConstraintGenerator:
"""Total demand on each route must not exceed vehicle capacity."""
for k in input_.vehicles:
yield (
pulp.lpSum(
input_.demand[i] * pulp.lpSum(variables.x[i, j, k] for j in input_.delta_out[i])
for i in input_.nodes
)
<= input_.vehicle_capacity,
f"Vehicle_capacity_{k}",
)
def set_up_objective(
self, input_: VRPTimeWindowsInput, prob: pulp.LpProblem, variables: VRPTimeWindowsVariables
) -> pulp.LpProblem:
prob += (
pulp.lpSum(
input_.cost[i][j] * pulp.lpSum(variables.x[i, j, k] for k in input_.vehicles) for (i, j) in input_.edges
),
"Total cost of the routes",
)
return prob
@hide_from_ai
@staticmethod
def display_description() -> None:
console = Console()
markdown_description = files(vrp_time_windows).joinpath("MODEL.md").read_text()
console.print(Markdown(markdown_description))
@hide_from_ai
@staticmethod
def get_description() -> str:
return files(vrp_time_windows).joinpath("MODEL.md").read_text()
The model class inherits from decision_ai.PulpDecisionAIModel, which provides the core optimization modeling capabilities. The constraints are defined using the decision_ai.constraint() decorator, which marks methods as constraint generators.
Complete Example#
# ruff: noqa: N805, N806
from __future__ import annotations
from collections import defaultdict
from importlib.resources import files
from typing import TYPE_CHECKING
import pandas as pd
import pulp
from pydantic import Field
from rich.console import Console
from rich.markdown import Markdown
from decision_ai import InputData, PulpDecisionAIModel, PulpVariables, constraint, hide_from_ai
from decision_ai.examples import vrp_time_windows
if TYPE_CHECKING:
from decision_ai import Solution
from decision_ai.typing import ConstraintGenerator
from pathlib import Path
class VRPTimeWindowsInput(InputData):
nodes: list[str]
edges: list[tuple[str, str]]
delta_out: dict[str, list[str]]
delta_in: dict[str, list[str]]
demand: dict[str, int]
latitudes: dict[str, float]
longitudes: dict[str, float]
service_time: dict[str, float]
time_window_start: dict[str, float]
time_window_end: dict[str, float]
travel_time: dict[str, dict[str, float]]
cost: dict[str, dict[str, float]]
vehicle_capacity: int
vehicles: list[str]
source: str
sink: str
@staticmethod
def from_csvs(path_to_directory: str | Path) -> VRPTimeWindowsInput:
"""Load VRP Time Windows input data from CSV files in the specified directory."""
path_to_directory = Path(path_to_directory)
# Load required CSV files
nodes_df = pd.read_csv(path_to_directory / "nodes.csv")
edges_df = pd.read_csv(path_to_directory / "edges.csv")
extra_df = pd.read_csv(path_to_directory / "extra.csv")
vehicles_df = pd.read_csv(path_to_directory / "vehicles.csv")
data = {"nodes": nodes_df, "edges": edges_df, "extra": extra_df, "vehicles": vehicles_df}
return VRPTimeWindowsInput.from_dataframes(data)
@staticmethod
def from_dataframes(data: dict[str, pd.DataFrame]) -> VRPTimeWindowsInput:
"""Convert pandas DataFrames into VRP Time Windows input data."""
nodes = data["nodes"]["node"].values.tolist()
source = nodes[0]
sink = nodes[-1]
demand = dict(zip(nodes, data["nodes"]["demand"], strict=False))
latitudes = dict(zip(nodes, data["nodes"]["lat"], strict=False))
longitudes = dict(zip(nodes, data["nodes"]["lon"], strict=False))
time_window_start = dict(zip(nodes, data["nodes"]["time_window_start"], strict=False))
time_window_end = dict(zip(nodes, data["nodes"]["time_window_end"], strict=False))
service_time = dict(zip(nodes, data["nodes"]["service_time"], strict=False))
edges, travel_time, cost = [], {}, {}
for _, row in data["edges"].iterrows():
from_, to_ = row["from"], row["to"]
edges.append((from_, to_))
edges.append((to_, from_))
# Create nested dictionary for travel_time
if from_ not in travel_time:
travel_time[from_] = {}
travel_time[from_][to_] = row["travel_time"]
if to_ not in travel_time:
travel_time[to_] = {}
travel_time[to_][from_] = row["travel_time"]
# Create a nested dictionary for cost
if from_ not in cost:
cost[from_] = {}
cost[from_][to_] = row["cost"]
if to_ not in cost:
cost[to_] = {}
cost[to_][from_] = row["cost"]
delta_in, delta_out = defaultdict(set), defaultdict(set)
for from_, to_ in edges:
delta_out[from_].add(to_)
delta_out[to_].add(from_)
delta_in[to_].add(from_)
delta_in[from_].add(to_)
for key in delta_in:
delta_in[key] = list(delta_in[key])
for key in delta_out:
delta_out[key] = list(delta_out[key])
vehicle_capacity = data["extra"][data["extra"]["field"] == "vehicle_capacity"]["value"].values[0]
vehicles = data["vehicles"]["vehicle"].values.tolist()
return VRPTimeWindowsInput(
nodes=nodes,
edges=edges,
delta_in=delta_in,
delta_out=delta_out,
demand=demand,
latitudes=latitudes,
longitudes=longitudes,
service_time=service_time,
time_window_start=time_window_start,
time_window_end=time_window_end,
travel_time=travel_time,
cost=cost,
vehicle_capacity=vehicle_capacity,
vehicles=vehicles,
source=source,
sink=sink,
)
@staticmethod
def load_example() -> VRPTimeWindowsInput:
"""Load example data.
Returns:
VRPTimeWindowsInput: The example data
"""
return VRPTimeWindowsInput.from_csvs(files(vrp_time_windows).joinpath("data_tables"))
class VRPTimeWindowsVariables(PulpVariables):
# index (node1, node2, vehicle). 1 if vehicle travels node1 -> node2, 0 otherwise
x: dict[tuple[str, str, str], pulp.LpVariable] = Field(
..., description="Flow variables. 1 if vehicle travels node1 -> node2, 0 otherwise"
)
# index (node). Service start time at node
service_start_time: dict[str, pulp.LpVariable] = Field(..., description="Service start time at node")
@staticmethod
def init_x(input_: VRPTimeWindowsInput) -> dict[tuple[str, str, str], pulp.LpVariable]:
return {
(node1, node2, vehicle): pulp.LpVariable(f"x_{node1}_{node2}_{vehicle}", cat=pulp.LpBinary)
for node1, node2 in input_.edges
for vehicle in input_.vehicles
}
@staticmethod
def init_service_start_time(input_: VRPTimeWindowsInput) -> dict[str, pulp.LpVariable]:
return {node: pulp.LpVariable(f"service_start_time_{node}", cat=pulp.LpContinuous) for node in input_.nodes}
class VRPTimeWindowsModel(PulpDecisionAIModel[VRPTimeWindowsInput, VRPTimeWindowsVariables]):
variables_class = VRPTimeWindowsVariables
def __init__(self):
super().__init__()
def solution_to_str(self, input_: VRPTimeWindowsInput, solution: Solution) -> str:
solution_display = "##### Solution\n\n"
solution_display += f"Objective value: {solution.objective}\n\n"
solution_display += f"Status: {solution.status}\n\n"
if solution.status == "Infeasible":
return solution_display
# Calculate the routes for each vehicle
routes = {}
for k in input_.vehicles:
routes[k] = [str(input_.source)]
node = input_.source
while node != input_.sink:
for j in input_.delta_out[node]:
if solution.variables.x[node, j, k] > 0.5:
routes[k].append(f"{j!s} ({round(solution.variables.service_start_time[j], 1)})")
node = j
break
# Show the routes
solution_display += "Routes: (in parenthesis the starting service time)\n"
for k in input_.vehicles:
solution_display += f"\n - {k}: {' -> '.join(routes[k])}"
return solution_display
@constraint
def customer_visited_once(input_: VRPTimeWindowsInput, variables: VRPTimeWindowsVariables) -> ConstraintGenerator:
"""Each customer must be visited exactly once, except source and sink."""
for i in input_.nodes:
if i in [input_.source, input_.sink]:
continue
yield (
pulp.lpSum(variables.x[i, j, k] for k in input_.vehicles for j in input_.delta_in[i]) == 1,
f"Customer_assignment_{i}",
)
@constraint
def source_out(input_: VRPTimeWindowsInput, variables: VRPTimeWindowsVariables) -> ConstraintGenerator:
"""Each vehicle must leave source once and never enter it."""
for k in input_.vehicles:
yield (
pulp.lpSum(variables.x[input_.source, j, k] for j in input_.delta_out[input_.source]) == 1,
f"Source_leave_{k}",
)
yield (
pulp.lpSum(variables.x[j, input_.source, k] for j in input_.delta_in[input_.source]) == 0,
f"Source_enter_{k}",
)
@constraint
def flow_conservation(input_: VRPTimeWindowsInput, variables: VRPTimeWindowsVariables) -> ConstraintGenerator:
"""Flow must be conserved at each node except source and sink."""
for j in input_.nodes:
if j in [input_.source, input_.sink]:
continue
for k in input_.vehicles:
yield (
pulp.lpSum(variables.x[i, j, k] for i in input_.delta_in[j])
- pulp.lpSum(variables.x[j, i, k] for i in input_.delta_out[j])
== 0,
f"Flow_conservation_{j}_{k}",
)
@constraint
def sink_in(input_: VRPTimeWindowsInput, variables: VRPTimeWindowsVariables) -> ConstraintGenerator:
"""Each vehicle must enter sink once and never leave it."""
for k in input_.vehicles:
yield (
pulp.lpSum(variables.x[i, input_.sink, k] for i in input_.delta_in[input_.sink]) == 1,
f"Sink_enter_{k}",
)
yield (
pulp.lpSum(variables.x[input_.sink, j, k] for j in input_.delta_out[input_.sink]) == 0,
f"Sink_leave_{k}",
)
@constraint
def service_time(input_: VRPTimeWindowsInput, variables: VRPTimeWindowsVariables) -> ConstraintGenerator:
"""Service time constraints between consecutive nodes."""
T = variables.service_start_time
M = {
(i, j): max(
input_.time_window_end[i]
- input_.time_window_start[j]
+ input_.service_time[i]
+ input_.travel_time[i][j],
0,
)
for (i, j) in input_.edges
}
for i, j in input_.edges:
if j == input_.source or i == input_.sink:
continue
for k in input_.vehicles:
yield (
T[i] + input_.service_time[i] + input_.travel_time[i][j] - T[j]
<= (1 - variables.x[i, j, k]) * M[i, j],
f"Service_time_{i}_{j}_{k}",
)
@constraint
def time_window(input_: VRPTimeWindowsInput, variables: VRPTimeWindowsVariables) -> ConstraintGenerator:
"""Service must start within the time window for each node."""
for i in input_.nodes:
yield (input_.time_window_start[i] <= variables.service_start_time[i], f"Time_window_start_{i}")
yield (variables.service_start_time[i] <= input_.time_window_end[i], f"Time_window_end_{i}")
@constraint
def vehicle_capacity(input_: VRPTimeWindowsInput, variables: VRPTimeWindowsVariables) -> ConstraintGenerator:
"""Total demand on each route must not exceed vehicle capacity."""
for k in input_.vehicles:
yield (
pulp.lpSum(
input_.demand[i] * pulp.lpSum(variables.x[i, j, k] for j in input_.delta_out[i])
for i in input_.nodes
)
<= input_.vehicle_capacity,
f"Vehicle_capacity_{k}",
)
def set_up_objective(
self, input_: VRPTimeWindowsInput, prob: pulp.LpProblem, variables: VRPTimeWindowsVariables
) -> pulp.LpProblem:
prob += (
pulp.lpSum(
input_.cost[i][j] * pulp.lpSum(variables.x[i, j, k] for k in input_.vehicles) for (i, j) in input_.edges
),
"Total cost of the routes",
)
return prob
@hide_from_ai
@staticmethod
def display_description() -> None:
console = Console()
markdown_description = files(vrp_time_windows).joinpath("MODEL.md").read_text()
console.print(Markdown(markdown_description))
@hide_from_ai
@staticmethod
def get_description() -> str:
return files(vrp_time_windows).joinpath("MODEL.md").read_text()