Skip to content

Commit 4ef4ce3

Browse files
Merge pull request #5 from nextmv-io/feature/nextroute-output
Add the output class to nextroute
2 parents 5bc56e8 + f84eadb commit 4ef4ce3

File tree

9 files changed

+2504
-2
lines changed

9 files changed

+2504
-2
lines changed

nextmv/nextroute/check/__init__.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""
2+
Check provides a plugin that allows you to check models and solutions.
3+
4+
Checking a model or a solution checks the unplanned plan units. It checks each
5+
individual plan unit if it can be added to the solution. If the plan unit can
6+
be added to the solution, the report will include on how many vehicles and
7+
what the impact would be on the objective value. If the plan unit cannot be
8+
added to the solution, the report will include the reason why it cannot be
9+
added to the solution.
10+
11+
The check can be invoked on a nextroute.Model or a nextroute.Solution. If the
12+
check is invoked on a model, an empty solution is created and the check is
13+
executed on this empty solution. An empty solution is a solution with all the
14+
initial stops that are fixed, initial stops that are not fixed are not added
15+
to the solution. The check is executed on the unplanned plan units of the
16+
solution. If the check is invoked on a solution, it is executed on the
17+
unplanned plan units of the solution.
18+
"""
19+
20+
from .schema import Objective as Objective
21+
from .schema import ObjectiveTerm as ObjectiveTerm
22+
from .schema import Output as Output
23+
from .schema import PlanUnit as PlanUnit
24+
from .schema import Solution as Solution
25+
from .schema import Summary as Summary
26+
from .schema import Vehicle as Vehicle

nextmv/nextroute/check/schema.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"""This module contains definitions for the schema in the Nextroute check."""
2+
3+
4+
from nextmv.base_model import BaseModel
5+
6+
7+
class ObjectiveTerm(BaseModel):
8+
"""Check of the individual terms of the objective for a move."""
9+
10+
base: float | None = None
11+
"""Base of the objective term."""
12+
factor: float | None = None
13+
"""Factor of the objective term."""
14+
name: str | None = None
15+
"""Name of the objective term."""
16+
value: float | None = None
17+
"""Value of the objective term, which is equivalent to `self.base *
18+
self.factor`."""
19+
20+
21+
class Objective(BaseModel):
22+
"""Estimate of an objective of a move."""
23+
24+
terms: list[ObjectiveTerm] | None = None
25+
"""Check of the individual terms of the objective."""
26+
value: float | None = None
27+
"""Value of the objective."""
28+
vehicle: str | None = None
29+
"""ID of the vehicle for which it reports the objective."""
30+
31+
32+
class Solution(BaseModel):
33+
"""Solution that the check has been executed on."""
34+
35+
objective: Objective | None = None
36+
"""Objective of the start solution."""
37+
plan_units_planned: int | None = None
38+
"""Number of plan units planned in the start solution."""
39+
plan_units_unplanned: int | None = None
40+
"""Number of plan units unplanned in the start solution."""
41+
stops_planned: int | None = None
42+
"""Number of stops planned in the start solution."""
43+
vehicles_not_used: int | None = None
44+
"""Number of vehicles not used in the start solution."""
45+
vehicles_used: int | None = None
46+
"""Number of vehicles used in the start solution."""
47+
48+
49+
class Summary(BaseModel):
50+
"""Summary of the check."""
51+
52+
moves_failed: int | None = None
53+
"""number of moves that failed. A move can fail if the estimate of a
54+
constraint is incorrect. A constraint is incorrect if `ModelConstraint.
55+
EstimateIsViolated` returns true and one of the violation checks returns
56+
false. Violation checks are implementations of one or more of the
57+
interfaces [SolutionStopViolationCheck], [SolutionVehicleViolationCheck] or
58+
[SolutionViolationCheck] on the same constraint. Most constraints do not
59+
need and do not have violation checks as the estimate is perfect. The
60+
number of moves failed can be more than one per plan unit as we continue to
61+
try moves on different vehicles until we find a move that is executable or
62+
all vehicles have been visited."""
63+
plan_units_best_move_failed: int | None = None
64+
"""Number of plan units for which the best move can not be planned. This
65+
should not happen if all the constraints are implemented correct."""
66+
plan_units_best_move_found: int | None = None
67+
"""Number of plan units for which at least one move has been found and the
68+
move is executable."""
69+
plan_units_best_move_increases_objective: int | None = None
70+
"""Number of plan units for which the best move is executable but would
71+
increase the objective value instead of decreasing it."""
72+
plan_units_checked: int | None = None
73+
"""Number of plan units that have been checked. If this is less than
74+
`self.plan_units_to_be_checked` the check timed out."""
75+
plan_units_have_no_move: int | None = None
76+
"""Number of plan units for which no feasible move has been found. This
77+
implies there is no move that can be executed without violating a
78+
constraint."""
79+
plan_units_to_be_checked: int | None = None
80+
"""Number of plan units to be checked."""
81+
82+
83+
class PlanUnit(BaseModel):
84+
"""Check of a plan unit."""
85+
86+
best_move_failed: bool | None = None
87+
"""True if the plan unit's best move failed to execute."""
88+
best_move_increases_objective: bool | None = None
89+
"""True if the best move for the plan unit increases the objective."""
90+
best_move_objective: Objective | None = None
91+
"""Estimate of the objective of the best move if the plan unit has a best
92+
move."""
93+
constraints: dict[str, int] | None = None
94+
"""Constraints that are violated for the plan unit."""
95+
has_best_move: bool | None = None
96+
"""True if a move is found for the plan unit. A plan unit has no move found
97+
if the plan unit is over-constrained or the move found is too expensive."""
98+
stops: list[str] | None = None
99+
"""IDs of the sops in the plan unit."""
100+
vehicles_have_moves: int | None = None
101+
"""Number of vehicles that have moves for the plan unit. Only calculated if
102+
the verbosity is very high."""
103+
vehicles_with_moves: list[str] | None = None
104+
"""IDs of the vehicles that have moves for the plan unit. Only calculated
105+
if the verbosity is very high."""
106+
107+
108+
class Vehicle(BaseModel):
109+
"""Check of a vehicle."""
110+
111+
id: str
112+
"""ID of the vehicle."""
113+
114+
plan_units_have_moves: int | None = None
115+
"""Number of plan units that have moves for the vehicle. Only calculated if
116+
the depth is medium."""
117+
118+
119+
class Output(BaseModel):
120+
"""Output of a feasibility check."""
121+
122+
duration_maximum: float | None = None
123+
"""Maximum duration of the check, in seconds."""
124+
duration_used: float | None = None
125+
"""Duration used by the check, in seconds."""
126+
error: str | None = None
127+
"""Error raised during the check."""
128+
plan_units: list[PlanUnit] | None = None
129+
"""Check of the individual plan units."""
130+
remark: str | None = None
131+
"""Remark of the check. It can be "ok", "timeout" or anything else that
132+
should explain itself."""
133+
solution: Solution | None = None
134+
"""Start soltuion of the check."""
135+
summary: Summary | None = None
136+
"""Summary of the check."""
137+
vehicles: list[Vehicle] | None = None
138+
"""Check of the vehicles."""
139+
verbosity: str | None = None
140+
"""Verbosity level of the check."""

nextmv/nextroute/schema/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@
44
from .input import DurationGroup as DurationGroup
55
from .input import Input as Input
66
from .location import Location as Location
7+
from .output import DataPoint as DataPoint
8+
from .output import ObjectiveOutput as ObjectiveOutput
9+
from .output import Output as Output
10+
from .output import PlannedStopOutput as PlannedStopOutput
11+
from .output import ResultStatistics as ResultStatistics
12+
from .output import RunStatistics as RunStatistics
13+
from .output import Series as Series
14+
from .output import SeriesData as SeriesData
15+
from .output import Solution as Solution
16+
from .output import Statistics as Statistics
17+
from .output import StopOutput as StopOutput
18+
from .output import VehicleOutput as VehicleOutput
19+
from .output import Version as Version
720
from .stop import AlternateStop as AlternateStop
821
from .stop import Stop as Stop
922
from .stop import StopDefaults as StopDefaults

nextmv/nextroute/schema/input.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Defines the input class"""
1+
"""Defines the input class."""
22

33
from typing import Any
44

nextmv/nextroute/schema/output.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
"""Defines the output class."""
2+
3+
4+
from datetime import datetime
5+
from typing import Any
6+
7+
from pydantic import Field
8+
9+
from nextmv.base_model import BaseModel
10+
from nextmv.nextroute.check import Output as checkOutput
11+
from nextmv.nextroute.schema.location import Location
12+
13+
14+
class Version(BaseModel):
15+
"""A version used for solving."""
16+
17+
sdk: str
18+
"""Nextmv SDK."""
19+
20+
21+
class StopOutput(BaseModel):
22+
"""Basic structure for the output of a stop."""
23+
24+
id: str
25+
"""ID of the stop."""
26+
location: Location
27+
"""Location of the stop."""
28+
29+
custom_data: Any | None = None
30+
"""Custom data of the stop."""
31+
32+
33+
class PlannedStopOutput(BaseModel):
34+
"""Output of a stop planned in the solution."""
35+
36+
stop: StopOutput
37+
"""Basic information on the stop."""
38+
39+
arrival_time: datetime | None = None
40+
"""Actual arrival time at this stop."""
41+
cumulative_travel_distance: float | None = None
42+
"""Cumulative distance to travel from the first stop to this one, in meters."""
43+
cumulative_travel_duration: float | None = None
44+
"""Cumulative duration to travel from the first stop to this one, in seconds."""
45+
custom_data: Any | None = None
46+
"""Custom data of the stop."""
47+
duration: float | None = None
48+
"""Duration of the service at the stop, in seconds."""
49+
early_arrival_duration: float | None = None
50+
"""Duration of early arrival at the stop, in seconds."""
51+
end_time: datetime | None = None
52+
"""End time of the service at the stop."""
53+
late_arrival_duration: float | None = None
54+
"""Duration of late arrival at the stop, in seconds."""
55+
mix_items: Any | None = None
56+
"""Mix items at the stop."""
57+
start_time: datetime | None = None
58+
"""Start time of the service at the stop."""
59+
target_arrival_time: datetime | None = None
60+
"""Target arrival time at this stop."""
61+
travel_distance: float | None = None
62+
"""Distance to travel from the previous stop to this one, in meters."""
63+
travel_duration: float | None = None
64+
"""Duration to travel from the previous stop to this one, in seconds."""
65+
waiting_duration: float | None = None
66+
"""Waiting duratino at the stop, in seconds."""
67+
68+
69+
class VehicleOutput(BaseModel):
70+
"""Output of a vehicle in the solution."""
71+
72+
id: str
73+
"""ID of the vehicle."""
74+
75+
alternate_stops: list[str] | None = None
76+
"""List of alternate stops that were planned on the vehicle."""
77+
custom_data: Any | None = None
78+
"""Custom data of the vehicle."""
79+
route: list[PlannedStopOutput] | None = None
80+
"""Route of the vehicle, which is a list of stops that were planned on
81+
it."""
82+
route_duration: float | None = None
83+
"""Total duration of the vehicle's route, in seconds."""
84+
route_stops_duration: float | None = None
85+
"""Total duration of the stops of the vehicle, in seconds."""
86+
route_travel_distance: float | None = None
87+
"""Total travel distance of the vehicle, in meters."""
88+
route_travel_duration: float | None = None
89+
"""Total travel duration of the vehicle, in seconds."""
90+
route_waiting_duration: float | None = None
91+
"""Total waiting duration of the vehicle, in seconds."""
92+
93+
94+
class ObjectiveOutput(BaseModel):
95+
"""Information of the objective (value function)."""
96+
97+
name: str
98+
"""Name of the objective."""
99+
100+
base: float | None = None
101+
"""Base of the objective."""
102+
custom_data: Any | None = None
103+
"""Custom data of the objective."""
104+
factor: float | None = None
105+
"""Factor of the objective."""
106+
objectives: list[dict[str, Any]] | None = None
107+
"""List of objectives. Each list is actually of the same class
108+
`ObjectiveOutput`, but we avoid a recursive definition here."""
109+
value: float | None = None
110+
"""Value of the objective, which is equivalent to `self.base *
111+
self.factor`."""
112+
113+
114+
class Solution(BaseModel):
115+
"""Solution to a Vehicle Routing Problem (VRP)."""
116+
117+
unplanned: list[StopOutput] | None = None
118+
"""List of stops that were not planned in the solution."""
119+
vehicles: list[VehicleOutput] | None = None
120+
"""List of vehicles in the solution."""
121+
objective: ObjectiveOutput | None = None
122+
"""Information of the objective (value function)."""
123+
check: checkOutput | None = None
124+
"""Check of the solution, if enabled."""
125+
126+
127+
class RunStatistics(BaseModel):
128+
"""Statistics about a general run."""
129+
130+
duration: float | None = None
131+
"""Duration of the run in seconds."""
132+
iterations: int | None = None
133+
"""Number of iterations."""
134+
custom: Any | None = None
135+
"""Custom statistics created by the user. Can normally expect a `dict[str,
136+
Any]`."""
137+
138+
139+
class ResultStatistics(BaseModel):
140+
"""Statistics about a specific result."""
141+
142+
duration: float | None = None
143+
"""Duration of the run in seconds."""
144+
value: float | None = None
145+
"""Value of the result."""
146+
custom: Any | None = None
147+
"""Custom statistics created by the user. Can normally expect a `dict[str,
148+
Any]`."""
149+
150+
151+
class DataPoint(BaseModel):
152+
"""A data point."""
153+
154+
x: float
155+
"""X coordinate of the data point."""
156+
y: float
157+
"""Y coordinate of the data point."""
158+
159+
160+
class Series(BaseModel):
161+
"""A series of data points."""
162+
163+
name: str | None = None
164+
"""Name of the series."""
165+
data_points: list[DataPoint] | None = None
166+
"""Data of the series."""
167+
168+
169+
class SeriesData(BaseModel):
170+
"""Data of a series."""
171+
172+
value: Series | None = None
173+
"""A series for the value of the solution."""
174+
custom: list[Series] | None = None
175+
"""A list of series for custom statistics."""
176+
177+
178+
class Statistics(BaseModel):
179+
"""Statistics of a solution."""
180+
181+
run: RunStatistics | None = None
182+
"""Statistics about the run."""
183+
result: ResultStatistics | None = None
184+
"""Statistics about the last result."""
185+
series_data: SeriesData | None = None
186+
"""Data of the series."""
187+
statistics_schema: str | None = Field(alias="schema")
188+
"""Schema (version)."""
189+
190+
191+
class Output(BaseModel):
192+
"""Output schema for Nextroute."""
193+
194+
options: dict[str, Any]
195+
"""Options used to obtain this output."""
196+
version: Version
197+
"""Versions used for the solution."""
198+
199+
solutions: list[Solution] | None = None
200+
"""Solutions to the problem."""
201+
statistics: Statistics | None = None
202+
"""Statistics of the solution."""

0 commit comments

Comments
 (0)