Skip to content

Utilities

Small helpers shared by the solvers, the dataset loaders and the test-suite: Bunch (the return type of the loaders), check_random_state and check_is_fitted (the two estimator helpers of the base class), initial_tour (the warm-start helper behind every init= parameter) and check_router, the structural test battery a new solver must pass before it is merged (tests/test_common.py runs it over skroute.all_solvers()). Its multi-trip check (8) also fits one instance with a per-node service_time (the depot's included) and requires the result to equal a fit on the time matrix with the services folded in by hand — the definition of D32 — so a solver must read problem.time_or_cost, never problem.time, when it prices durations during its search; check 6 exercises the service_time error messages, so a fit override must forward it.

utils

Shared helpers: Bunch, input validation, the warm-start helper and the estimator checks.

  • Bunch — dict with attribute access (the return type of the dataset loaders).
  • check_random_state — None/int/Generator -> numpy.random.Generator.
  • check_is_fitted — raises NotFittedError before fit.
  • initial_tour — builds a solver's starting tour from its init parameter.
  • check_router — the structural test battery (in skroute.utils.estimator_checks; also exported as skroute.check_router).

Bunch

Bunch(**kwargs)

Bases: dict

Container object exposing keys as attributes.

Bunch extends dict so that values can be accessed either with b["key"] or b.key. Setting an attribute stores a key.

Examples:

>>> from skroute.utils import Bunch
>>> b = Bunch(a=1, b="two")
>>> b.a, b["b"]
(1, 'two')
>>> b.c = [3]
>>> sorted(b)
['a', 'b', 'c']

initial_tour

initial_tour(problem: RoutingProblem, init: Any, rng: Generator | None) -> ndarray

Build the starting index tour of a solver from its init parameter.

Parameters:

Name Type Description Default
problem RoutingProblem

The instance being solved.

required
init ('nearest_neighbour', 'random')

"nearest_neighbour" runs the core's greedy nearest-neighbour construction from the depot; "random" returns the depot followed by a random permutation of the other nodes (requires rng); an array of labels — the tour_ or route_ of another solver, open, closed or multi-trip — is converted with to_index_tour.

"nearest_neighbour"
rng Generator or None

The solver's generator; None for deterministic solvers.

required

Returns:

Name Type Description
tour ndarray of shape (n,), int64

A permutation of range(n) with problem.depot at position 0.

Raises:

Type Description
ValueError

If init is an unknown string or not iterable at all (None, a number), or "random" is requested without rng (the solver is not stochastic), or the label array is not a valid tour.

Examples:

>>> import numpy as np
>>> from skroute import RoutingProblem
>>> from skroute.utils import initial_tour
>>> C = np.array([[0, 5, 9, 10], [5, 0, 4, 8], [9, 4, 0, 3], [10, 8, 3, 0]], dtype=float)
>>> p = RoutingProblem(C, depot=3)
>>> initial_tour(p, "nearest_neighbour", None).tolist()
[3, 2, 1, 0]
>>> initial_tour(p, [3, 0, 1, 2, 3], None).tolist()
[3, 0, 1, 2]
>>> t = initial_tour(p, "random", np.random.default_rng(0))
>>> int(t[0]) == 3 and sorted(t.tolist()) == [0, 1, 2, 3]
True

check_is_fitted

check_is_fitted(estimator: Any) -> None

Raise NotFittedError unless estimator has been fitted.

An estimator is fitted when it carries the cost_ attribute, which fit sets last.

Parameters:

Name Type Description Default
estimator BaseRouter

The estimator to check.

required

Raises:

Type Description
NotFittedError

With the message "This <Name> instance is not fitted yet. Call 'fit' first.".

Examples:

>>> from skroute.base import BaseRouter
>>> from skroute.utils import check_is_fitted
>>> class Identity(BaseRouter):
...     def _solve(self, problem, rng):
...         return problem.to_index_tour(problem.labels)
>>> check_is_fitted(Identity())
Traceback (most recent call last):
    ...
skroute.exceptions.NotFittedError: This Identity instance is not fitted yet. Call 'fit' first.
>>> check_is_fitted(Identity().fit([[0, 1, 2], [1, 0, 1], [2, 1, 0]]))

check_random_state

check_random_state(seed: Any) -> Generator

Turn seed into a numpy.random.Generator.

Parameters:

Name Type Description Default
seed (None, int or Generator)

None or an integer are passed to numpy.random.default_rng; a Generator is returned as is (and is therefore advanced by the fit that uses it). The legacy RandomState is not accepted.

required

Returns:

Name Type Description
rng Generator

Raises:

Type Description
TypeError

If seed is of any other kind (bool included).

Examples:

>>> from skroute.utils import check_random_state
>>> rng = check_random_state(0)
>>> int(rng.integers(0, 10)) == int(check_random_state(0).integers(0, 10))
True
>>> check_random_state(rng) is rng
True

check_router

check_router(estimator: BaseRouter) -> None

Run the structural test battery of SPEC §6 on an unfitted solver instance.

Parameters:

Name Type Description Default
estimator BaseRouter

An unfitted instance (so MultiStart(SimulatedAnnealing()) can be checked too). The battery never fits it: every check works on clones with random_state=0.

required

Raises:

Type Description
AssertionError

Prefixed with the number of the failing check ("check 3: ...").

TypeError

If estimator is not a BaseRouter instance.

ValueError

If estimator is already fitted.

Warns:

Type Description
UserWarning

For every check that had to be skipped (skroute.datasets unavailable).

Notes

The checks: 1 parameter protocol, 2 not fitted, 3 fitted attributes, 4 recomputed cost, 5 input kinds, 6 invalid inputs, 7 tags, 8 multi-trip, 9 no printing, 10 iterative contract, 11 reproducibility, 13 smallest sizes, 14 callback protocol (D30). check_router.checks lists them as (name, fn) pairs; the tolerance checks (12) live in tests/test_common.py.

Examples:

>>> from skroute import check_router
>>> from skroute.base import BaseRouter, RouterTags
>>> import numpy as np
>>> class Identity(BaseRouter):
...     def __init__(self, verbose=0):
...         self.verbose = verbose
...
...     def _get_tags(self):
...         return RouterTags(kind="construction")
...
...     def _solve(self, problem, rng):
...         return np.roll(np.arange(problem.n), -problem.depot)
>>> check_router(Identity())
>>> [name for name, fn in check_router.checks][:3]
['1_init_and_params', '2_not_fitted', '3_fit_results']