Base and problem¶
Every solver in scikit-route is an estimator: __init__ stores the hyper-parameters (the
"knobs"), fit(X, ...) receives the data and returns self, and the results live in
trailing-underscore attributes (tour_, route_, trips_, cost_, ...). The contract has
three parts:
RoutingProblem— one immutable instance in index space: the coerced cost matrix, the optional time matrix and budget, the depot and the labels.fitbuilds it for you, or you build it once and pass it to several solvers (Other().fit(est.problem_)).BaseRouter— the template method: it validates the inputs and the hyper-parameters, honours the solver'sRouterTags, calls the solver's_solve(problem, rng), validates the returned tour and recomputescost_from it, so a route that does not match its cost is impossible by construction.RouteEvent— the progress report a solver hands to thecallbackoffit, so you can watch (or stop) a search while it runs.- the helpers
clone,is_router,all_solversandset_log_level, the exceptions and the public helpers ofskroute.metrics:route_costandsplit_tripsrecompute a label-space route,timetableturns a solution into arrival and departure times (one list ofStops per day) andtimetable_summarytotals every day.
Plain TSP is est.fit(C); the multi-trip objective is
est.fit(C, time_matrix=T, max_time_work=8, extra_cost=12.83, people=2) — time_matrix is
keyword-only, so the two square matrices can never be swapped by accident.
Service times¶
A visit takes time: fit(..., service_time=0.5) (a scalar for every non-depot node) or
service_time=array (one value per node, in matrix row order, the depot's paid once per day at
departure) adds it to the budget, in the units of time_matrix (D32). The kernels search on the
effective matrix T_eff[i, j] = T[i, j] + service[j] for j != depot, T_eff[i, depot] = T[i, depot]
and T_eff[depot, j] += service[depot]; problem_.time stays the raw driving time,
problem_.service_time is the (n,) array (zeros when not given), problem_.time_or_cost is
T_eff, and trip_times_ includes the visits. A fit with service_time equals, by definition, a
fit on T_eff without it; route_cost(..., service_time=) re-prices a route the same way.
>>> import numpy as np
>>> from skroute import BruteForce
>>> from skroute.metrics import timetable
>>> C = np.array([[0, 5, 9, 10], [5, 0, 4, 8], [9, 4, 0, 3], [10, 8, 3, 0]], dtype=float)
>>> hours = np.array([[0, 1, 2, 2], [1, 0, 1, 2], [2, 1, 0, 1], [2, 2, 1, 0]], dtype=float)
>>> bf = BruteForce().fit(C, time_matrix=hours, max_time_work=5.0, extra_cost=3.0, service_time=0.5)
>>> bf.route_.tolist(), bf.trip_times_.tolist(), bf.cost_
([0, 1, 2, 0, 3, 0], [5.0, 4.5], 41.0)
>>> [(s.label, s.arrival_time, s.departure_time) for s in timetable(bf, units="h")[0]]
[(0, '08:00', '08:00'), (1, '09:00', '09:30'), (2, '10:30', '11:00'), (0, '13:00', '13:00')]
Watching a solver work¶
Every fit accepts callback=: any callable taking one RouteEvent. The solver calls it once
at "start", once after every outer iteration — exactly where history_ grows, with the tour it
is working on, the best tour so far and their costs — and once at "end" with the fitted tour.
Return True to stop the search after the current outer iteration (stop_reason_ == "callback").
The tours arrive in label space, depot first, and event.route/event.trips decode the best one
with the problem's own split rule, so a multi-trip instance can be drawn trip by trip. Every
solver documents the keys of event.extra it fills (the temperature of an annealing level, the
kick of an iterated local search, the LP support of a MILP cut round...). Inside a
MultiStart the inner solvers report under their own name with extra["restart"], and only when
the restarts run sequentially (n_jobs=None or 1 — then in the calling thread, whatever an
enclosing joblib.parallel_config says, so the callback never runs in a worker). The callback
runs inside the timed search (fit_time_ includes it), and an exception it raises propagates out
of fit and leaves the estimator unfitted, at the "end" event as at any other. The
skroute.viz package builds live plots and animations on top of this protocol.
>>> import numpy as np
>>> from skroute import SimulatedAnnealing
>>> C = np.array([[0, 5, 9, 10], [5, 0, 4, 8], [9, 4, 0, 3], [10, 8, 3, 0]], dtype=float)
>>> seen = []
>>> sa = SimulatedAnnealing(random_state=0).fit(C, callback=seen.append)
>>> seen[0].stage, seen[0].iteration, seen[-1].stage, seen[-1].iteration == sa.n_iter_
('start', 0, 'end', True)
>>> len(seen) == sa.n_iter_ + 2 and seen[-1].best_cost == sa.cost_
True
>>> sorted(seen[1].extra)
['accepted', 'n_moves', 'temperature']
>>> short = SimulatedAnnealing(random_state=0).fit(C, callback=lambda e: e.iteration >= 5)
>>> short.n_iter_, short.stop_reason_
(5, 'callback')
Partial structures: edges, edge_weights, ring¶
A solver that has no tour yet still has something to show — and three optional extra keys
carry it, in a shape every viewer understands (D31); keys a viewer does not know are ignored.
extra["edges"]— a list of(label, label)tuples: the partial structure the solver holds whentourisNoneor incomplete. The construction heuristics emit one"iteration"event per construction step, withtour=None,cost=nanandbest_cost=nan(nan means unknown, not a value): the growing path ofNearestNeighbour(n - 1events), the closed partial cycle ofInsertionafter each placement (n - 1, the seed included), the currentdepot -> trip -> depotlegs ofClarkeWrightbefore the first merge and after each accepted one (merges + 1, plusextra["n_trips"]), the edge set ofNRBSafter each connection (n, plusextra["n_edges"]);MILPreports the support of each cut round;AntColonyits strongestmin(3n, n(n-1)/2)pheromone trails (min(3n, n(n-1))arcs on an asymmetric matrix, where every arc competes).extra["edge_weights"]— floats in[0, 1]parallel toextra["edges"]:AntColony's trail strength over the currenttau_max,MILP's variable values of the support.extra["ring"]— an(m, 2)float array:SOM's neuron positions after each epoch, in the units ofproblem.coords, so the ring can be drawn over the cities.
The traces cost nothing without a callback: the kernels record nothing and the Python side replays
nothing. Every event owns its list or array — a recorder that keeps the events keeps the history.
A construction solver has no search to stop: a callback returning True only silences its remaining
step events (all of them when it answers at "start"), and the result never depends on the callback.
>>> import math
>>> from skroute import NearestNeighbour
>>> trace = []
>>> nn = NearestNeighbour().fit(C, labels=["d", "a", "b", "c"], callback=trace.append)
>>> [e.stage for e in trace]
['start', 'iteration', 'iteration', 'iteration', 'end']
>>> [e.extra["edges"] for e in trace if e.stage == "iteration"]
[[('d', 'a')], [('d', 'a'), ('a', 'b')], [('d', 'a'), ('a', 'b'), ('b', 'c')]]
>>> trace[1].tour is None and math.isnan(trace[1].cost), trace[-1].best_tour.tolist()
(True, ['d', 'a', 'b', 'c'])
RoutingProblem ¶
RoutingProblem(X: Any, *, time_matrix: Any = None, depot: Any = None, coords: Any = None, labels: Any = None, max_time_work: float | None = None, extra_cost: float = 0.0, people: int = 1, service_time: Any = None, split: str = 'greedy')
One instance in index space. Immutable after construction; shareable across solvers and threads.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
(n, n) array-like, DataFrame or dict-of-dicts
|
Cost matrix. Rows are origins, columns destinations. Must be square, n >= 3, all finite. |
required |
time_matrix
|
same kinds as X, optional, keyword-only (D7)
|
Durations, same shape and labels as X, all finite and >= 0. Required iff max_time_work is given. |
None
|
depot
|
label
|
Label of the depot (a position for plain arrays without labels=). Default: first node. |
None
|
coords
|
(n, 2) array-like
|
Coordinates in row order of X. Needed by SOM; carried, never validated beyond shape. |
None
|
labels
|
sequence of n hashables
|
Labels for a plain ndarray X. Must equal the labels X already carries, if any. |
None
|
max_time_work
|
float > 0
|
Per-trip budget in the units of time_matrix. None = plain TSP. |
None
|
extra_cost
|
float >= 0
|
Fixed charge per trip beyond the first. |
0.0
|
people
|
int >= 1
|
Multiplies extra_cost only. |
1
|
service_time
|
float or (n,) array-like
|
Time spent at each node, in the units of |
None
|
split
|
('greedy', 'optimal')
|
Decoder of the giant tour into trips (see D1). |
"greedy"
|
Attributes:
| Name | Type | Description |
|---|---|---|
cost |
ndarray of shape (n, n), float64, C-contiguous
|
The coerced cost matrix. |
time |
ndarray of shape (n, n) or None
|
The coerced raw travel-time matrix; |
service_time |
ndarray of shape (n,), float64
|
Service time of every node in matrix row order; all zeros when not given. |
n |
int
|
Number of nodes. |
labels |
ndarray of shape (n,), dtype int64 or object
|
Label of every node, in matrix row order. |
depot |
int
|
Index of the depot. |
max_time_work |
float
|
The per-trip budget; |
extra_cost |
float
|
|
people |
int
|
|
split |
{'greedy', 'optimal'}
|
|
coords |
ndarray of shape (n, 2) or None
|
|
symmetric |
bool
|
Whether |
Notes
The objective (D1) is the travel cost of the decoded trips plus
people * extra_cost * (n_trips - 1). Under the greedy decoder a leg a -> b
joins the open trip iff the trip can still return to the depot within
max_time_work; the optimal decoder (Prins, 2004) is the minimum-cost
partition of the giant tour into consecutive feasible trips. Both are O(n)
and O(n L) respectively, with L the longest feasible open path.
A service time is folded into the matrix the decoders read (D32): with s the
(n,) service array and d the depot, the effective time matrix is
T_eff[i, j] = T[i, j] + s[j] for j != d (the service is paid on arrival),
T_eff[i, d] = T[i, d] (nothing is paid on returning) and T_eff[d, j] += s[d]
for j != d (a service at the depot is paid once per trip, at departure); the
diagonal, which no kernel reads, stays raw. time_or_cost returns T_eff, so
evaluate, trip_starts and trip_times all account for the services, and
fitting with service_time equals fitting on T_eff without it. time
keeps the raw travel times for reporting (skroute.metrics.timetable).
The problem never writes to its arrays, but it does not copy a float64, C-contiguous
X or time_matrix either — cost and time are then views of the caller's
arrays (no second n x n matrix at n = 10 639) — so an array must not be modified after
it has been passed. service_time is always copied, and with a non-zero service
T_eff is a private matrix: a later write to the caller's time_matrix would move
timetable (which reads time) but not trip_times_ (which reads T_eff).
References
.. [1] C. Prins, "A simple and effective evolutionary algorithm for the vehicle routing problem", Computers & Operations Research 31 (2004) 1985-2002.
Examples:
>>> import numpy as np
>>> from skroute import RoutingProblem
>>> C = np.array([[0, 5, 9, 10], [5, 0, 4, 8], [9, 4, 0, 3], [10, 8, 3, 0]], dtype=float)
>>> p = RoutingProblem(C, labels=["d", "a", "b", "c"], depot="d")
>>> p
RoutingProblem(n=4, TSP, symmetric, depot='d')
>>> p.to_index_tour(["d", "a", "b", "c", "d"]).tolist()
[0, 1, 2, 3]
>>> p.evaluate([0, 1, 2, 3])
22.0
>>> hours = np.array([[0, 1, 2, 2], [1, 0, 1, 2], [2, 1, 0, 1], [2, 2, 1, 0]], dtype=float)
>>> q = RoutingProblem(C, time_matrix=hours, max_time_work=4.0, extra_cost=3.0)
>>> q.evaluate([0, 1, 2, 3]), q.trip_starts([0, 1, 2, 3]).tolist()
(41.0, [1, 3, 4])
Half an hour at every customer under a five-hour day: the services are paid on
arrival (row 0 of the effective matrix), never on the way back (column 0), and
trip_times includes them.
>>> r = RoutingProblem(C, time_matrix=hours, max_time_work=5.0, extra_cost=3.0, service_time=0.5)
>>> r.service_time.tolist()
[0.0, 0.5, 0.5, 0.5]
>>> r.time_or_cost[0].tolist(), r.time_or_cost[:, 0].tolist()
([0.0, 1.5, 2.5, 2.5], [0.0, 1.0, 2.0, 2.0])
>>> starts = r.trip_starts([0, 1, 2, 3])
>>> starts.tolist(), r.trip_times([0, 1, 2, 3], starts).tolist(), r.evaluate([0, 1, 2, 3])
([1, 3, 4], [5.0, 4.5], 41.0)
time_or_cost
property
¶
The matrix kernels receive as T: the effective time matrix, or the cost matrix if no budget.
The effective matrix is time plus the service times (D32, see the class notes): it is
time when no service was given. Without a budget (max_time_work == inf) the kernels
never read T; passing the cost matrix keeps every call signature uniform.
index_of ¶
Index of label in matrix row order; ValueError if it is not a label of X.
to_index_tour ¶
Labels -> int64 tour with the depot at position 0.
Accepts an open tour (n labels), a closed route (depot repeated at the end) or a multi-trip route (depot repeated between trips): every occurrence of the depot is removed, then the depot is prepended. Raises ValueError unless the remaining labels are exactly the non-depot labels, each once.
to_label_tour ¶
Index tour -> label array (same length, label dtype).
evaluate ¶
Objective of an index tour (D1). O(n) greedy / plain, O(n*L) optimal.
trip_starts ¶
Trip start positions of an index tour.
Returns an int64 array of shape (n_trips + 1,) with starts[0] == 1 and starts[-1] == n;
trip k is tour[starts[k]:starts[k + 1]]. Plain TSP gives [1, n].
trip_costs ¶
Travel cost of each closed trip (fixed charge excluded), float64 (n_trips,).
trip_times ¶
Duration of each closed trip, float64 (n_trips,), services included. Requires a time matrix.
neighbours ¶
k nearest neighbours of every node by C[i, :], as int64 (n, k), sorted ascending; cached.
The diagonal is excluded regardless of its value (only finiteness is required of it) and ties are broken by node index (stable sort). Six bundled instances have coincident points (lu980: 346 duplicate rows, ho14473: 7 370), so zero off-diagonal distances are normal input.
BaseRouter ¶
Base class of every solver.
Subclasses: store every __init__ argument verbatim as an attribute of the same name and
nothing else; declare _parameter_constraints; override _get_tags(); implement
_solve(problem, rng) -> int64 array (a permutation of range(n) with problem.depot first).
Iterative solvers set self.history_, self.n_iter_, self.stop_reason_ inside _solve;
exact solvers set self.is_optimal_. Everything else is set here.
Attributes:
| Name | Type | Description |
|---|---|---|
problem_ |
RoutingProblem
|
The coerced instance (reusable: |
n_nodes_ |
int
|
Number of nodes. |
labels_ |
ndarray of shape (n,), label dtype
|
Labels in matrix row order. |
depot_ |
scalar, label dtype
|
The depot's label. |
tour_ |
ndarray of shape (n,), label dtype
|
Open giant tour, depot first — the warm-start format ( |
route_ |
ndarray of shape (n + n_trips,), label dtype
|
As driven: depot, trip 1, depot, trip 2, ..., depot. |
trips_ |
list of ndarray
|
One closed |
n_trips_ |
int
|
|
trip_costs_ |
ndarray of shape (n_trips,), float64
|
Travel cost of each closed trip (fixed charge excluded). |
trip_times_ |
ndarray of shape (n_trips,), float64
|
Only when a time matrix was given; each |
cost_ |
float
|
|
fit_time_ |
float
|
Seconds spent in |
history_ |
ndarray of shape (n_iter_,), float64
|
Iterative solvers only: best-so-far cost after each outer iteration. |
n_iter_ |
int
|
Iterative solvers only: outer iterations actually run. |
stop_reason_ |
str
|
Iterative solvers only: |
is_optimal_ |
bool
|
Exact solvers only. |
Notes
fit never trusts a cost reported by the solver: the returned index tour is
validated (a permutation with the depot first, else RuntimeError) and
cost_ is recomputed with the problem's own decoder, so a route that does not
match its cost is impossible by construction.
Examples:
>>> import numpy as np
>>> from skroute.base import BaseRouter, RouterTags
>>> class Identity(BaseRouter):
... '''Returns the nodes in matrix order.'''
...
... def __init__(self, verbose=0):
... self.verbose = verbose
...
... def _get_tags(self):
... return RouterTags(kind="construction")
...
... def _solve(self, problem, rng):
... return np.arange(problem.n)
>>> C = np.array([[0, 5, 9, 10], [5, 0, 4, 8], [9, 4, 0, 3], [10, 8, 3, 0]], dtype=float)
>>> est = Identity().fit(C)
>>> est.tour_.tolist(), est.route_.tolist(), est.cost_, est.n_trips_
([0, 1, 2, 3], [0, 1, 2, 3, 0], 22.0, 1)
>>> est
Identity()
>>> Identity(verbose=1)
Identity(verbose=1)
get_params ¶
Parameters of this estimator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
deep
|
bool
|
If True, also return the parameters of nested estimators as |
True
|
Returns:
| Name | Type | Description |
|---|---|---|
params |
dict
|
Parameter names mapped to their values. |
set_params ¶
set_params(**params: Any) -> BaseRouter
Set the parameters of this estimator (<name>__<param> reaches nested estimators).
Returns:
| Type | Description |
|---|---|
self
|
|
fit ¶
fit(X: Any, *, time_matrix: Any = None, depot: Any = None, coords: Any = None, labels: Any = None, max_time_work: float | None = None, extra_cost: float = 0.0, people: int = 1, service_time: Any = None, split: str = 'greedy', callback: Callable[[RouteEvent], Any] | None = None) -> BaseRouter
Solve the instance and store the result in trailing-underscore attributes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
(n, n) array-like, DataFrame, dict-of-dicts or RoutingProblem
|
Cost matrix (rows are origins). A ready |
required |
time_matrix
|
same kinds as X, optional, keyword-only
|
Durations; required iff |
None
|
depot
|
label
|
Label of the depot. Default: the first node. |
None
|
coords
|
(n, 2) array-like
|
Node coordinates in row order (needed by |
None
|
labels
|
sequence of n hashables
|
Labels for a plain ndarray |
None
|
max_time_work
|
float > 0
|
Per-trip budget in the units of |
None
|
extra_cost
|
float >= 0
|
Fixed charge per trip beyond the first. |
0.0
|
people
|
int >= 1
|
Multiplies |
1
|
service_time
|
float or (n,) array-like
|
Time spent at each stop, in the units of |
None
|
split
|
('greedy', 'optimal')
|
Decoder of the giant tour into trips. |
"greedy"
|
callback
|
callable
|
Called with one |
None
|
Returns:
| Type | Description |
|---|---|
self
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
RouterTags
dataclass
¶
RouterTags(kind: str = 'metaheuristic', exact: bool = False, stochastic: bool = False, iterative: bool = False, budget_aware: bool = False, requires_symmetric: bool = False, requires_coords: bool = False, max_nodes: int | None = None)
Capabilities of a solver, returned by BaseRouter._get_tags().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
kind
|
('exact', 'construction', 'local_search', 'metaheuristic', 'ensemble')
|
Family of the solver (D28); used by the capability table and the tolerance tests. |
"exact"
|
exact
|
bool
|
Provably optimal for the objective it accepts; such solvers set |
False
|
stochastic
|
bool
|
Consumes |
False
|
iterative
|
bool
|
Sets |
False
|
budget_aware
|
bool
|
The search itself sees the multi-trip objective. Solvers opt IN (D28): a solver that does not declare it warns under a budget and its result is still decoded and priced under the multi-trip objective (D6). |
False
|
requires_symmetric
|
bool
|
Raises on an asymmetric cost matrix ( |
False
|
requires_coords
|
bool
|
Raises without |
False
|
max_nodes
|
int or None
|
Hard cap on the number of nodes ( |
None
|
Notes
budget_aware=True is declared explicitly by: BruteForce, ClarkeWright, TwoOpt,
OrOpt, LocalSearch, IteratedLocalSearch, SimulatedAnnealing, TabuSearch, Genetic,
AntColony, EnsembleGenetic, EnsembleSimulatedAnnealing; MultiStart delegates to its
estimator. Everything else warns under a budget.
Examples:
>>> from skroute import RouterTags
>>> RouterTags(kind="exact", exact=True, budget_aware=True, max_nodes=11).budget_aware
True
>>> RouterTags().budget_aware
False
RouteEvent
dataclass
¶
RouteEvent(solver: str, stage: str, iteration: int, cost: float, best_cost: float, tour: ndarray | None, best_tour: ndarray | None, problem: RoutingProblem, extra: dict[str, Any] = dict())
One progress report of a running solver, handed to the callback of fit (D30).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
solver
|
str
|
Class name of the solver that emitted the event ( |
required |
stage
|
('start', 'iteration', 'end')
|
|
"start"
|
iteration
|
int
|
Outer iteration index: 0 at |
required |
cost
|
float
|
Objective of |
required |
best_cost
|
float
|
Objective of |
required |
tour
|
ndarray of shape (n,) or None
|
The solver's CURRENT solution as a label-space open giant tour, depot first — the tour it
is working on (the annealing walker, the candidate of an iterated local search, the best
individual of a generation...); |
required |
best_tour
|
ndarray of shape (n,) or None
|
The best-so-far tour in the same format; at |
required |
problem
|
RoutingProblem
|
The instance being solved ( |
required |
extra
|
dict
|
Solver-specific facts: |
dict()
|
Notes
Events are frozen and compare by identity. The arrays are copies: a solver's buffers keep
changing after the callback returns, the event does not. route and trips decode
best_tour with the problem's own split rule (the one fit uses for route_ and
trips_), so a multi-trip instance is drawn trip by trip.
Examples:
A deterministic descent on four nodes: one event per stage, and the "end" event carries
the fitted tour.
>>> import numpy as np
>>> from skroute import TwoOpt
>>> C = np.array([[0, 5, 9, 10], [5, 0, 4, 8], [9, 4, 0, 3], [10, 8, 3, 0]], dtype=float)
>>> events = []
>>> est = TwoOpt().fit(C, callback=events.append)
>>> [e.stage for e in events]
['start', 'iteration', 'end']
>>> events[-1]
RouteEvent(solver='TwoOpt', stage='end', iteration=1, best_cost=22)
>>> events[-1].best_tour.tolist() == est.tour_.tolist(), events[-1].route.tolist()
(True, [0, 1, 2, 3, 0])
Returning True stops an iterative solver after the current outer iteration:
clone ¶
clone(estimator: BaseRouter) -> BaseRouter
New unfitted estimator with the same parameters (deep copies nothing; parameters are values).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
estimator
|
BaseRouter
|
The estimator to copy. Nested estimators (parameters with |
required |
Returns:
| Name | Type | Description |
|---|---|---|
estimator |
BaseRouter
|
A fresh instance of the same class, |
Examples:
>>> from skroute import clone
>>> from skroute.base import BaseRouter
>>> class Identity(BaseRouter):
... def __init__(self, verbose=0):
... self.verbose = verbose
...
... def _solve(self, problem, rng):
... return problem.to_index_tour(problem.labels)
>>> est = Identity(verbose=2).fit([[0, 1, 2], [1, 0, 1], [2, 1, 0]])
>>> copy = clone(est)
>>> copy == est, copy is est, hasattr(copy, "cost_")
(True, False, False)
is_router ¶
Whether obj is a scikit-route estimator (an instance of BaseRouter).
Examples:
all_solvers ¶
all_solvers() -> list[type[BaseRouter]]
Every solver class that can be instantiated with no arguments, sorted by name (D27).
The roster is derived from the registry at the top of skroute/__init__.py: every
registered name defined in one of the five solver subpackages (exact,
construction, local_search, metaheuristics, ensemble) except
MultiStart, which needs an estimator. Imports are eager: a registered module that
is missing or does not define its name raises ImportError — the roster is the
parametrisation of the whole test battery and must never shrink silently.
Returns:
| Name | Type | Description |
|---|---|---|
solvers |
list of type
|
Subclasses of |
Raises:
| Type | Description |
|---|---|
ImportError
|
When a registered solver module cannot be imported or lacks the registered name. |
Examples:
set_log_level ¶
Set the level of the skroute logger and make its records visible.
Sets logging.getLogger("skroute").setLevel(level); if the logger's only handler is
the NullHandler attached at import, adds a StreamHandler to stderr with the
format "%(name)s %(levelname)s %(message)s". Equivalent to
logging.basicConfig(level=logging.INFO) for users who do not configure logging.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
level
|
int or str
|
A logging level ( |
required |
Examples:
>>> import logging, skroute
>>> log = logging.getLogger("skroute")
>>> saved = (log.level, list(log.handlers)) # the example restores the logger it changes
>>> skroute.set_log_level("INFO")
>>> log.level == logging.INFO and any(isinstance(h, logging.StreamHandler) for h in log.handlers)
True
>>> log.setLevel(saved[0]) # undo, so the example leaves the logger as it found it
>>> log.handlers[:] = saved[1]
exceptions ¶
Exceptions raised by scikit-route.
NotFittedError ¶
Bases: ValueError, AttributeError
Raised by check_is_fitted when a fitted attribute is
accessed before fit was called.
InfeasibleProblemError ¶
Bases: ValueError
Raised when a node cannot be served in a single trip within max_time_work.
metrics ¶
Public recomputation helpers: route_cost,
split_trips and the per-day
timetable (SPEC §3.5, D32).
Everything here works in label space, on the route_/tour_ arrays a fitted
solver exposes, so a user (or a test) can price any route under the objective — or
turn it into arrival and departure times — without touching index space.
Stop
dataclass
¶
Stop(day: int, order: int, label: Any, arrival: float, departure: float, travel: float, service: float, wait: float = 0.0, start: float = 8 * 60.0)
One line of a timetable: a visit, or the depot at either end of a day.
Times are minutes since the start of the day (floats, so the arithmetic is exact and
the caller decides the rounding); the *_time properties render them as HH:MM
clock strings from the day's start.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
day
|
int
|
Day (trip) number, 1-based. |
required |
order
|
int
|
Position in the day: |
required |
label
|
hashable
|
Label of the node (the depot's label at both ends). |
required |
arrival
|
float
|
Minutes after |
required |
departure
|
float
|
|
required |
travel
|
float
|
Driving minutes from the previous stop ( |
required |
service
|
float
|
Minutes spent at the stop ( |
required |
wait
|
float
|
Idle minutes before the service starts. Always |
0.0
|
start
|
float
|
Minutes after midnight at which the day starts ( |
480.0
|
Examples:
>>> from skroute.metrics import Stop
>>> stop = Stop(day=1, order=2, label="b", arrival=95.0, departure=125.0, travel=20.0, service=30.0)
>>> stop.arrival_time, stop.departure_time
('09:35', '10:05')
>>> Stop(1, 0, "d", 0.0, 0.0, 0.0, 0.0, start=9 * 60 + 30).departure_time
'09:30'
>>> Stop(1, 0, "d", 0.0, 0.0, 0.0, 0.0, start=9 * 60 + 30) == Stop(1, 0, "d", 0.0, 0.0, 0.0, 0.0)
True
route_cost ¶
route_cost(X: Any, route: Any, *, depot: Any = None, labels: Any = None, time_matrix: Any = None, max_time_work: float | None = None, extra_cost: float = 0.0, people: int = 1, service_time: Any = None, split: str = 'greedy') -> float
Objective of a label-space route (D1), recomputed from scratch.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
(n, n) array-like, DataFrame or dict-of-dicts
|
Cost matrix (rows are origins). |
required |
route
|
sequence of labels
|
Open tour, closed route or multi-trip route (the depot may repeat). Its first
label is the depot. Every depot occurrence is removed and the giant tour is
re-decoded with |
required |
depot
|
label
|
Must equal |
None
|
labels
|
sequence of n hashables
|
Labels of a plain ndarray |
None
|
time_matrix
|
same kinds as X
|
Durations; required iff |
None
|
max_time_work
|
float > 0
|
Per-trip budget. |
None
|
extra_cost
|
float >= 0
|
Fixed charge per trip beyond the first. |
0.0
|
people
|
int >= 1
|
Multiplies |
1
|
service_time
|
float or (n,) array-like
|
Time spent at each stop, in the units of |
None
|
split
|
('greedy', 'optimal')
|
Decoder of the giant tour into trips. |
"greedy"
|
Returns:
| Name | Type | Description |
|---|---|---|
cost |
float
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
|
Examples:
>>> from skroute.metrics import route_cost
>>> C = [[0, 5, 9, 10], [5, 0, 4, 8], [9, 4, 0, 3], [10, 8, 3, 0]]
>>> route_cost(C, [0, 1, 2, 3, 0])
22.0
>>> route_cost(C, [2, 3, 0, 1], labels=[0, 1, 2, 3])
22.0
>>> hours = [[0, 1, 2, 2], [1, 0, 1, 2], [2, 1, 0, 1], [2, 2, 1, 0]]
>>> route_cost(C, [0, 1, 2, 0, 3, 0], time_matrix=hours, max_time_work=4.0, extra_cost=3.0)
41.0
Half an hour at every stop makes the same tour need two trips under a five-hour day:
split_trips ¶
Split a label route at depot occurrences into closed trips [depot, ..., depot].
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
route
|
sequence of labels
|
As driven: depot first, possibly repeated between trips and at the end. An open tour (depot only at the front) is returned as one closed trip. |
required |
depot
|
label
|
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
trips |
list of ndarray
|
One closed trip per segment, in route order; empty segments (two consecutive depots) are dropped. |
Examples:
timetable ¶
timetable(obj: Any, route: Any = ..., *, start: Any = ..., units: str = ..., as_frame: Literal[False] = ...) -> list[list[Stop]]
timetable(obj: Any, route: Any = ..., *, start: Any = ..., units: str = ..., as_frame: Literal[True]) -> DataFrame
timetable(obj: Any, route: Any = ..., *, start: Any = ..., units: str = ..., as_frame: bool) -> list[list[Stop]] | DataFrame
timetable(obj: Any, route: Any = None, *, start: Any = '08:00', units: str = 'min', as_frame: bool = False) -> list[list[Stop]] | DataFrame
Arrival and departure times of every stop, day by day (D32).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
obj
|
fitted estimator or RoutingProblem
|
A fitted solver (its |
required |
route
|
sequence of labels
|
Required with a |
None
|
start
|
str or time
|
Start of the day at the depot, every day, as |
"08:00"
|
units
|
('min', 'h', 's')
|
Units of |
"min"
|
as_frame
|
bool
|
Return one flat |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
days |
list of list of Stop, or DataFrame
|
One inner list per day (trip), each opening with the departure from the depot
( |
Raises:
| Type | Description |
|---|---|
ValueError
|
|
TypeError
|
When |
NotFittedError
|
For an unfitted estimator. |
Warns:
| Type | Description |
|---|---|
UserWarning
|
When a day of a route read as driven ends after |
See Also
timetable_summary : the totals of every day.
Examples:
Minutes of driving between four nodes, a 30-minute service at every customer and a day of 200 minutes: the fitted tour needs two days.
>>> import numpy as np
>>> from skroute import BruteForce
>>> from skroute.metrics import timetable
>>> minutes = np.array([[0, 30, 60, 60], [30, 0, 30, 60], [60, 30, 0, 30], [60, 60, 30, 0]], dtype=float)
>>> est = BruteForce().fit(
... minutes,
... labels=["office", "a", "b", "c"],
... time_matrix=minutes,
... max_time_work=200.0,
... service_time=30.0,
... )
>>> est.route_.tolist()
['office', 'a', 'b', 'office', 'c', 'office']
>>> days = timetable(est, start="09:00")
>>> for stop in days[0]:
... print(stop.order, stop.label, stop.arrival_time, stop.departure_time, stop.travel, stop.service)
0 office 09:00 09:00 0.0 0.0
1 a 09:30 10:00 30.0 30.0
2 b 10:30 11:00 30.0 30.0
3 office 12:00 12:00 60.0 0.0
>>> [(d[-1].arrival, d[-1].arrival_time) for d in days] # each day ends within 200 minutes
[(180.0, '12:00'), (150.0, '11:30')]
The same from a problem and a route (any tour, decoded with the problem's split rule), and as a table when pandas is installed:
>>> problem = est.problem_
>>> [[s.label for s in day] for day in timetable(problem, ["office", "c", "b", "a"])]
[['office', 'c', 'office'], ['office', 'b', 'a', 'office']]
>>> timetable(est, as_frame=True)[["day", "label", "arrival_time", "departure_time"]]
day label arrival_time departure_time
0 1 office 08:00 08:00
1 1 a 08:30 09:00
...
timetable_summary ¶
timetable_summary(days: list[list[Stop]]) -> list[dict[str, Any]]
Totals of every day of a timetable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
days
|
list of list of Stop
|
As returned by |
required |
Returns:
| Name | Type | Description |
|---|---|---|
summary |
list of dict
|
One dict per day with |
Raises:
| Type | Description |
|---|---|
TypeError
|
When |
Examples:
>>> import numpy as np
>>> from skroute import BruteForce
>>> from skroute.metrics import timetable, timetable_summary
>>> minutes = np.array([[0, 30, 60, 60], [30, 0, 30, 60], [60, 30, 0, 30], [60, 60, 30, 0]], dtype=float)
>>> est = BruteForce().fit(minutes, time_matrix=minutes, max_time_work=200.0, service_time=30.0)
>>> for day in timetable_summary(timetable(est)):
... print(day)
{'day': 1, 'n_stops': 2, 'driving': 120.0, 'service': 60.0, 'total': 180.0, 'back_at': '11:00'}
{'day': 2, 'n_stops': 1, 'driving': 120.0, 'service': 30.0, 'total': 150.0, 'back_at': '10:30'}